Compare commits

..

No commits in common. "aaa1f333bfe364e2407fa520b76c49eb6e96bbca" and "702896a512f76be90234f913d7c26af256d9a6f3" have entirely different histories.

16 changed files with 9 additions and 1088 deletions

View file

@ -111,7 +111,6 @@ var migrations = []*Migration{
NewMigration("Noop because of https://codeberg.org/forgejo/forgejo/issues/8373", NoopAddIndexToActionRunStopped),
// v35 -> v36
NewMigration("Fix wiki unit default permission", FixWikiUnitDefaultPermission),
NewMigration("Add `branch_filter` to `push_mirror` table", AddPushMirrorBranchFilter),
}
// GetCurrentDBVersion returns the current Forgejo database version.

View file

@ -1,16 +0,0 @@
// Copyright 2025 The Forgejo Authors. All rights reserved.
// SPDX-License-Identifier: GPL-3.0-or-later
package forgejo_migrations
import (
"xorm.io/xorm"
)
func AddPushMirrorBranchFilter(x *xorm.Engine) error {
type PushMirror struct {
ID int64 `xorm:"pk autoincr"`
BranchFilter string `xorm:"VARCHAR(255)"`
}
return x.Sync2(new(PushMirror))
}

View file

@ -32,7 +32,6 @@ type PushMirror struct {
Repo *Repository `xorm:"-"`
RemoteName string
RemoteAddress string `xorm:"VARCHAR(2048)"`
BranchFilter string `xorm:"VARCHAR(2048)"`
// A keypair formatted in OpenSSH format.
PublicKey string `xorm:"VARCHAR(100)"`
@ -123,11 +122,6 @@ func UpdatePushMirrorInterval(ctx context.Context, m *PushMirror) error {
return err
}
func UpdatePushMirrorBranchFilter(ctx context.Context, m *PushMirror) error {
_, err := db.GetEngine(ctx).ID(m.ID).Cols("branch_filter").Update(m)
return err
}
var DeletePushMirrors = deletePushMirrors
func deletePushMirrors(ctx context.Context, opts PushMirrorOptions) error {

View file

@ -75,139 +75,3 @@ func TestPushMirrorPrivatekey(t *testing.T) {
assert.Empty(t, actualPrivateKey)
})
}
func TestPushMirrorBranchFilter(t *testing.T) {
require.NoError(t, unittest.PrepareTestDatabase())
t.Run("Create push mirror with branch filter", func(t *testing.T) {
m := &repo_model.PushMirror{
RepoID: 1,
RemoteName: "test-branch-filter",
BranchFilter: "main,develop",
}
unittest.AssertSuccessfulInsert(t, m)
assert.NotZero(t, m.ID)
assert.Equal(t, "main,develop", m.BranchFilter)
})
t.Run("Create push mirror with empty branch filter", func(t *testing.T) {
m := &repo_model.PushMirror{
RepoID: 1,
RemoteName: "test-empty-filter",
BranchFilter: "",
}
unittest.AssertSuccessfulInsert(t, m)
assert.NotZero(t, m.ID)
assert.Empty(t, m.BranchFilter)
})
t.Run("Create push mirror without branch filter", func(t *testing.T) {
m := &repo_model.PushMirror{
RepoID: 1,
RemoteName: "test-no-filter",
// BranchFilter: "",
}
unittest.AssertSuccessfulInsert(t, m)
assert.NotZero(t, m.ID)
assert.Empty(t, m.BranchFilter)
})
t.Run("Update branch filter", func(t *testing.T) {
m := &repo_model.PushMirror{
RepoID: 1,
RemoteName: "test-update",
BranchFilter: "main",
}
unittest.AssertSuccessfulInsert(t, m)
m.BranchFilter = "main,develop"
require.NoError(t, repo_model.UpdatePushMirrorBranchFilter(db.DefaultContext, m))
updated := unittest.AssertExistsAndLoadBean(t, &repo_model.PushMirror{ID: m.ID})
assert.Equal(t, "main,develop", updated.BranchFilter)
})
t.Run("Retrieve push mirror with branch filter", func(t *testing.T) {
original := &repo_model.PushMirror{
RepoID: 1,
RemoteName: "test-retrieve",
BranchFilter: "main,develop",
}
unittest.AssertSuccessfulInsert(t, original)
retrieved := unittest.AssertExistsAndLoadBean(t, &repo_model.PushMirror{ID: original.ID})
assert.Equal(t, original.BranchFilter, retrieved.BranchFilter)
assert.Equal(t, "main,develop", retrieved.BranchFilter)
})
t.Run("GetPushMirrorsByRepoID includes branch filter", func(t *testing.T) {
mirrors := []*repo_model.PushMirror{
{
RepoID: 2,
RemoteName: "mirror-1",
BranchFilter: "main",
},
{
RepoID: 2,
RemoteName: "mirror-2",
BranchFilter: "develop,feature-*",
},
{
RepoID: 2,
RemoteName: "mirror-3",
BranchFilter: "",
},
}
for _, mirror := range mirrors {
unittest.AssertSuccessfulInsert(t, mirror)
}
retrieved, count, err := repo_model.GetPushMirrorsByRepoID(db.DefaultContext, 2, db.ListOptions{})
require.NoError(t, err)
assert.Equal(t, int64(3), count)
assert.Len(t, retrieved, 3)
filterMap := make(map[string]string)
for _, mirror := range retrieved {
filterMap[mirror.RemoteName] = mirror.BranchFilter
}
assert.Equal(t, "main", filterMap["mirror-1"])
assert.Equal(t, "develop,feature-*", filterMap["mirror-2"])
assert.Empty(t, filterMap["mirror-3"])
})
t.Run("GetPushMirrorsSyncedOnCommit includes branch filter", func(t *testing.T) {
mirrors := []*repo_model.PushMirror{
{
RepoID: 3,
RemoteName: "sync-mirror-1",
BranchFilter: "main,develop",
SyncOnCommit: true,
},
{
RepoID: 3,
RemoteName: "sync-mirror-2",
BranchFilter: "feature-*",
SyncOnCommit: true,
},
}
for _, mirror := range mirrors {
unittest.AssertSuccessfulInsert(t, mirror)
}
retrieved, err := repo_model.GetPushMirrorsSyncedOnCommit(db.DefaultContext, 3)
require.NoError(t, err)
assert.Len(t, retrieved, 2)
filterMap := make(map[string]string)
for _, mirror := range retrieved {
filterMap[mirror.RemoteName] = mirror.BranchFilter
}
assert.Equal(t, "main,develop", filterMap["sync-mirror-1"])
assert.Equal(t, "feature-*", filterMap["sync-mirror-2"])
})
}

View file

@ -13,7 +13,6 @@ type CreatePushMirrorOption struct {
Interval string `json:"interval"`
SyncOnCommit bool `json:"sync_on_commit"`
UseSSH bool `json:"use_ssh"`
BranchFilter string `json:"branch_filter"`
}
// PushMirror represents information of a push mirror
@ -30,6 +29,4 @@ type PushMirror struct {
Interval string `json:"interval"`
SyncOnCommit bool `json:"sync_on_commit"`
PublicKey string `json:"public_key"`
BranchFilter string `json:"branch_filter"`
}

View file

@ -55,8 +55,6 @@
"repo.form.cannot_create": "All spaces in which you can create repositories have reached the limit of repositories.",
"repo.issue_indexer.title": "Issue Indexer",
"search.milestone_kind": "Search milestones…",
"repo.settings.push_mirror.branch_filter.label": "Branch filter (optional)",
"repo.settings.push_mirror.branch_filter.description": "Branches to be mirrored. Leave blank to mirror all branches. See <a href=\"%[1]s\">%[2]s</a> documentation for syntax. Examples: <code>main, release/*</code>",
"incorrect_root_url": "This Forgejo instance is configured to be served on \"%s\". You are currently viewing Forgejo through a different URL, which may cause parts of the application to break. The canonical URL is controlled by Forgejo admins via the ROOT_URL setting in the app.ini.",
"themes.names.forgejo-auto": "Forgejo (follow system theme)",
"themes.names.forgejo-light": "Forgejo light",

View file

@ -389,7 +389,6 @@ func CreatePushMirror(ctx *context.APIContext, mirrorOption *api.CreatePushMirro
Interval: interval,
SyncOnCommit: mirrorOption.SyncOnCommit,
RemoteAddress: remoteAddress,
BranchFilter: mirrorOption.BranchFilter,
}
var plainPrivateKey []byte

View file

@ -6,7 +6,6 @@
package setting
import (
go_context "context"
"errors"
"fmt"
"net/http"
@ -590,23 +589,6 @@ func SettingsPost(ctx *context.Context) {
ctx.ServerError("UpdatePushMirrorInterval", err)
return
}
if m.BranchFilter != form.PushMirrorBranchFilter {
// replace `remote.<remote>.push` in config and db
m.BranchFilter = form.PushMirrorBranchFilter
if err := db.WithTx(ctx, func(ctx go_context.Context) error {
// Update the DB
if err = repo_model.UpdatePushMirrorBranchFilter(ctx, m); err != nil {
return err
}
// Update the repo config
return mirror_service.UpdatePushMirrorBranchFilter(ctx, m)
}); err != nil {
ctx.ServerError("UpdatePushMirrorBranchFilter", err)
return
}
}
// Background why we are adding it to Queue
// If we observed its implementation in the context of `push-mirror-sync` where it
// is evident that pushing to the queue is necessary for updates.
@ -702,7 +684,6 @@ func SettingsPost(ctx *context.Context) {
SyncOnCommit: form.PushMirrorSyncOnCommit,
Interval: interval,
RemoteAddress: remoteAddress,
BranchFilter: form.PushMirrorBranchFilter,
}
var plainPrivateKey []byte

View file

@ -23,6 +23,5 @@ func ToPushMirror(ctx context.Context, pm *repo_model.PushMirror) (*api.PushMirr
Interval: pm.Interval.String(),
SyncOnCommit: pm.SyncOnCommit,
PublicKey: pm.GetPublicKey(),
BranchFilter: pm.BranchFilter,
}, nil
}

View file

@ -141,7 +141,6 @@ type RepoSettingForm struct {
PushMirrorSyncOnCommit bool
PushMirrorInterval string
PushMirrorUseSSH bool
PushMirrorBranchFilter string `binding:"MaxSize(2048)" preprocess:"TrimSpace"`
Private bool
Template bool
EnablePrune bool

View file

@ -33,22 +33,19 @@ var AddPushMirrorRemote = addPushMirrorRemote
func addPushMirrorRemote(ctx context.Context, m *repo_model.PushMirror, addr string) error {
addRemoteAndConfig := func(addr, path string) error {
var cmd *git.Command
if m.BranchFilter == "" {
cmd = git.NewCommand(ctx, "remote", "add", "--mirror").AddDynamicArguments(m.RemoteName, addr)
} else {
cmd = git.NewCommand(ctx, "remote", "add").AddDynamicArguments(m.RemoteName, addr)
}
cmd := git.NewCommand(ctx, "remote", "add", "--mirror=push").AddDynamicArguments(m.RemoteName, addr)
if strings.Contains(addr, "://") && strings.Contains(addr, "@") {
cmd.SetDescription(fmt.Sprintf("remote add %s %s [repo_path: %s]", m.RemoteName, util.SanitizeCredentialURLs(addr), path))
cmd.SetDescription(fmt.Sprintf("remote add %s --mirror=push %s [repo_path: %s]", m.RemoteName, util.SanitizeCredentialURLs(addr), path))
} else {
cmd.SetDescription(fmt.Sprintf("remote add %s %s [repo_path: %s]", m.RemoteName, addr, path))
cmd.SetDescription(fmt.Sprintf("remote add %s --mirror=push %s [repo_path: %s]", m.RemoteName, addr, path))
}
if _, _, err := cmd.RunStdString(&git.RunOpts{Dir: path}); err != nil {
return err
}
err := addRemotePushRefSpecs(ctx, path, m)
if err != nil {
if _, _, err := git.NewCommand(ctx, "config", "--add").AddDynamicArguments("remote."+m.RemoteName+".push", "+refs/heads/*:refs/heads/*").RunStdString(&git.RunOpts{Dir: path}); err != nil {
return err
}
if _, _, err := git.NewCommand(ctx, "config", "--add").AddDynamicArguments("remote."+m.RemoteName+".push", "+refs/tags/*:refs/tags/*").RunStdString(&git.RunOpts{Dir: path}); err != nil {
return err
}
return nil
@ -70,49 +67,6 @@ func addPushMirrorRemote(ctx context.Context, m *repo_model.PushMirror, addr str
return nil
}
func addRemotePushRefSpecs(ctx context.Context, path string, m *repo_model.PushMirror) error {
if m.BranchFilter == "" {
// If there is no branch filter, set the push refspecs to mirror all branches and tags.
if _, _, err := git.NewCommand(ctx, "config", "--add").AddDynamicArguments("remote."+m.RemoteName+".push", "+refs/heads/*:refs/heads/*").RunStdString(&git.RunOpts{Dir: path}); err != nil {
return err
}
} else {
branches := strings.SplitSeq(m.BranchFilter, ",")
for branch := range branches {
branch = strings.TrimSpace(branch)
if branch == "" {
continue
}
refspec := fmt.Sprintf("+refs/heads/%s:refs/heads/%s", branch, branch)
if _, _, err := git.NewCommand(ctx, "config", "--add").AddDynamicArguments("remote."+m.RemoteName+".push", refspec).RunStdString(&git.RunOpts{Dir: path}); err != nil {
return err
}
}
}
if _, _, err := git.NewCommand(ctx, "config", "--add").AddDynamicArguments("remote."+m.RemoteName+".push", "+refs/tags/*:refs/tags/*").RunStdString(&git.RunOpts{Dir: path}); err != nil {
return err
}
return nil
}
func UpdatePushMirrorBranchFilter(ctx context.Context, m *repo_model.PushMirror) error {
path := m.Repo.RepoPath()
// First, remove all existing push refspecs for this remote
cmd := git.NewCommand(ctx, "config", "--unset-all").AddDynamicArguments("remote." + m.RemoteName + ".push")
if _, _, err := cmd.RunStdString(&git.RunOpts{Dir: path}); err != nil {
// Ignore error if the key doesn't exist
if !strings.Contains(err.Error(), "does not exist") {
return err
}
}
err := addRemotePushRefSpecs(ctx, path, m)
if err != nil {
return err
}
return nil
}
// RemovePushMirrorRemote removes the push mirror remote.
func RemovePushMirrorRemote(ctx context.Context, m *repo_model.PushMirror) error {
cmd := git.NewCommand(ctx, "remote", "rm").AddDynamicArguments(m.RemoteName)
@ -258,6 +212,7 @@ func runPushSync(ctx context.Context, m *repo_model.PushMirror) error {
return util.SanitizeErrorCredentialURLs(err)
}
return nil
}

View file

@ -254,7 +254,6 @@
data-modal-push-mirror-edit-id="{{.ID}}"
data-modal-push-mirror-edit-interval="{{.Interval}}"
data-modal-push-mirror-edit-address="{{.RemoteAddress}}"
data-modal-push-mirror-edit-branch-filter="{{.BranchFilter}}"
>
{{svg "octicon-pencil" 14}}
</button>
@ -289,11 +288,6 @@
<input id="push_mirror_address" name="push_mirror_address" value="{{.push_mirror_address}}" required>
<p class="help">{{ctx.Locale.Tr "repo.mirror_address_desc"}}</p>
</div>
<div class="field {{if .Err_PushMirrorBranchFilter}}error{{end}}">
<label for="push_mirror_branch_filter">{{ctx.Locale.Tr "repo.settings.push_mirror.branch_filter.label"}}</label>
<input id="push_mirror_branch_filter" name="push_mirror_branch_filter" value="{{.push_mirror_branch_filter}}" maxlength="2048">
<p class="help">{{ctx.Locale.Tr "repo.settings.push_mirror.branch_filter.description" "https://forgejo.org/docs/latest/user/repo-mirror/#branch-filter" "forgejo"}}</p>
</div>
<details class="ui optional field" {{if or .Err_PushMirrorAuth .push_mirror_username}}open{{end}}>
<summary class="tw-p-1">
{{ctx.Locale.Tr "repo.need_auth"}}

View file

@ -15,16 +15,7 @@
</div>
<div class="inline field">
<label for="push-mirror-edit-interval">{{ctx.Locale.Tr "repo.mirror_interval" .MinimumMirrorInterval}}</label>
<div class="ui small input">
<input id="push-mirror-edit-interval" name="push_mirror_interval" autofocus>
</div>
</div>
<div class="field">
<label for="push-mirror-edit-branch-filter">{{ctx.Locale.Tr "repo.settings.push_mirror.branch_filter.label"}}</label>
<div class="ui small input">
<input id="push-mirror-edit-branch-filter" name="push_mirror_branch_filter" maxlength="2048">
</div>
<p class="help">{{ctx.Locale.Tr "repo.settings.push_mirror.branch_filter.description" "https://forgejo.org/docs/latest/user/repo-mirror/#branch-filter" "forgejo"}}</p>
<input id="push-mirror-edit-interval" name="push_mirror_interval" autofocus>
</div>
<div class="actions">
<button class="ui small basic cancel button">

View file

@ -23111,10 +23111,6 @@
"type": "object",
"title": "CreatePushMirrorOption represents need information to create a push mirror of a repository.",
"properties": {
"branch_filter": {
"type": "string",
"x-go-name": "BranchFilter"
},
"interval": {
"type": "string",
"x-go-name": "Interval"
@ -26998,10 +26994,6 @@
"description": "PushMirror represents information of a push mirror",
"type": "object",
"properties": {
"branch_filter": {
"type": "string",
"x-go-name": "BranchFilter"
},
"created": {
"type": "string",
"format": "date-time",

View file

@ -14,7 +14,6 @@ import (
"os/exec"
"path/filepath"
"strconv"
"strings"
"testing"
"time"
@ -143,159 +142,6 @@ func testAPIPushMirror(t *testing.T, u *url.URL) {
}
}
func TestAPIPushMirrorBranchFilter(t *testing.T) {
onGiteaRun(t, testAPIPushMirrorBranchFilter)
}
func testAPIPushMirrorBranchFilter(t *testing.T, u *url.URL) {
defer test.MockVariableValue(&setting.Migrations.AllowLocalNetworks, true)()
defer test.MockVariableValue(&setting.Mirror.Enabled, true)()
defer test.MockProtect(&mirror_service.AddPushMirrorRemote)()
defer test.MockProtect(&repo_model.DeletePushMirrors)()
require.NoError(t, migrations.Init())
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1})
srcRepo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
owner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: srcRepo.OwnerID})
session := loginUser(t, user.Name)
token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeAll)
urlStr := fmt.Sprintf("/api/v1/repos/%s/%s/push_mirrors", owner.Name, srcRepo.Name)
mirrorRepo, _, f := tests.CreateDeclarativeRepo(t, user, "", []unit.Type{unit.TypeCode}, nil, nil)
defer f()
remoteAddress := fmt.Sprintf("%s%s/%s", u.String(), url.PathEscape(user.Name), url.PathEscape(mirrorRepo.Name))
t.Run("Create push mirror with branch filter", func(t *testing.T) {
req := NewRequestWithJSON(t, "POST", urlStr, &api.CreatePushMirrorOption{
RemoteAddress: remoteAddress,
Interval: "8h",
BranchFilter: "main,develop",
}).AddTokenAuth(token)
MakeRequest(t, req, http.StatusOK)
// Verify the push mirror was created with branch filter
req = NewRequest(t, "GET", urlStr).AddTokenAuth(token)
resp := MakeRequest(t, req, http.StatusOK)
var pushMirrors []*api.PushMirror
DecodeJSON(t, resp, &pushMirrors)
require.Len(t, pushMirrors, 1)
assert.Equal(t, "main,develop", pushMirrors[0].BranchFilter)
// Cleanup
req = NewRequest(t, "DELETE", fmt.Sprintf("%s/%s", urlStr, pushMirrors[0].RemoteName)).AddTokenAuth(token)
MakeRequest(t, req, http.StatusNoContent)
})
t.Run("Create push mirror with empty branch filter", func(t *testing.T) {
req := NewRequestWithJSON(t, "POST", urlStr, &api.CreatePushMirrorOption{
RemoteAddress: remoteAddress,
Interval: "8h",
BranchFilter: "",
}).AddTokenAuth(token)
MakeRequest(t, req, http.StatusOK)
// Verify the push mirror was created with empty branch filter
req = NewRequest(t, "GET", urlStr).AddTokenAuth(token)
resp := MakeRequest(t, req, http.StatusOK)
var pushMirrors []*api.PushMirror
DecodeJSON(t, resp, &pushMirrors)
require.Len(t, pushMirrors, 1)
assert.Empty(t, pushMirrors[0].BranchFilter)
// Cleanup
req = NewRequest(t, "DELETE", fmt.Sprintf("%s/%s", urlStr, pushMirrors[0].RemoteName)).AddTokenAuth(token)
MakeRequest(t, req, http.StatusNoContent)
})
t.Run("Create push mirror without branch filter parameter", func(t *testing.T) {
req := NewRequestWithJSON(t, "POST", urlStr, &api.CreatePushMirrorOption{
RemoteAddress: remoteAddress,
Interval: "8h",
// BranchFilter: ""
}).AddTokenAuth(token)
MakeRequest(t, req, http.StatusOK)
// Verify the push mirror defaults to empty branch filter
req = NewRequest(t, "GET", urlStr).AddTokenAuth(token)
resp := MakeRequest(t, req, http.StatusOK)
var pushMirrors []*api.PushMirror
DecodeJSON(t, resp, &pushMirrors)
require.Len(t, pushMirrors, 1)
assert.Empty(t, pushMirrors[0].BranchFilter)
// Cleanup
req = NewRequest(t, "DELETE", fmt.Sprintf("%s/%s", urlStr, pushMirrors[0].RemoteName)).AddTokenAuth(token)
MakeRequest(t, req, http.StatusNoContent)
})
t.Run("Retrieve multiple push mirrors with different branch filters", func(t *testing.T) {
// Create multiple push mirrors with different branch filters
testCases := []struct {
name string
branchFilter string
}{
{"mirror-1", "main"},
{"mirror-2", "develop,feature-*"},
{"mirror-3", ""},
}
// Create mirrors
mirrorCleanups := []func(){}
defer func() {
for _, mirror := range mirrorCleanups {
mirror()
}
}()
for _, tc := range testCases {
mirrorRepo, _, f := tests.CreateDeclarativeRepo(t, user, tc.name, []unit.Type{unit.TypeCode}, nil, nil)
mirrorCleanups = append(mirrorCleanups, f)
remoteAddr := fmt.Sprintf("%s%s/%s", u.String(), url.PathEscape(user.Name), url.PathEscape(mirrorRepo.Name))
req := NewRequestWithJSON(t, "POST", urlStr, &api.CreatePushMirrorOption{
RemoteAddress: remoteAddr,
Interval: "8h",
BranchFilter: tc.branchFilter,
}).AddTokenAuth(token)
MakeRequest(t, req, http.StatusOK)
}
// Retrieve all mirrors and verify branch filters
req := NewRequest(t, "GET", urlStr).AddTokenAuth(token)
resp := MakeRequest(t, req, http.StatusOK)
var pushMirrors []*api.PushMirror
DecodeJSON(t, resp, &pushMirrors)
require.Len(t, pushMirrors, 3)
// Create a map for easier verification
filterMap := make(map[string]string)
var createdMirrors []*api.PushMirror
for _, mirror := range pushMirrors {
for _, tc := range testCases {
if strings.Contains(mirror.RemoteAddress, tc.name) {
filterMap[tc.name] = mirror.BranchFilter
createdMirrors = append(createdMirrors, mirror)
break
}
}
}
assert.Equal(t, "main", filterMap["mirror-1"])
assert.Equal(t, "develop,feature-*", filterMap["mirror-2"])
assert.Empty(t, filterMap["mirror-3"])
// Cleanup
for _, mirror := range createdMirrors {
req = NewRequest(t, "DELETE", fmt.Sprintf("%s/%s", urlStr, mirror.RemoteName)).AddTokenAuth(token)
MakeRequest(t, req, http.StatusNoContent)
}
})
}
func TestAPIPushMirrorSSH(t *testing.T) {
_, err := exec.LookPath("ssh")
if err != nil {

View file

@ -13,12 +13,10 @@ import (
"os/exec"
"path/filepath"
"strconv"
"strings"
"testing"
"time"
asymkey_model "forgejo.org/models/asymkey"
auth_model "forgejo.org/models/auth"
"forgejo.org/models/db"
repo_model "forgejo.org/models/repo"
"forgejo.org/models/unit"
@ -28,7 +26,6 @@ import (
"forgejo.org/modules/gitrepo"
"forgejo.org/modules/optional"
"forgejo.org/modules/setting"
api "forgejo.org/modules/structs"
"forgejo.org/modules/test"
gitea_context "forgejo.org/services/context"
doctor "forgejo.org/services/doctor"
@ -37,7 +34,6 @@ import (
repo_service "forgejo.org/services/repository"
"forgejo.org/tests"
"github.com/PuerkitoBio/goquery"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
@ -138,27 +134,6 @@ func doCreatePushMirror(ctx APITestContext, address, username, password string)
}
}
func doCreatePushMirrorWithBranchFilter(ctx APITestContext, address, username, password, branchFilter 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",
"push_mirror_branch_filter": branchFilter,
})
ctx.Session.MakeRequest(t, req, http.StatusSeeOther)
flashCookie := ctx.Session.GetCookie(gitea_context.CookieNameFlash)
assert.NotNil(t, flashCookie)
assert.Contains(t, flashCookie.Value, "success")
}
}
func doRemovePushMirror(ctx APITestContext, address, username, password string, pushMirrorID int) 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)))
@ -347,196 +322,6 @@ func TestSSHPushMirror(t *testing.T) {
})
}
func TestPushMirrorBranchFilterWebUI(t *testing.T) {
onGiteaRun(t, func(t *testing.T, u *url.URL) {
defer test.MockVariableValue(&setting.Migrations.AllowLocalNetworks, true)()
defer test.MockVariableValue(&setting.Mirror.Enabled, true)()
require.NoError(t, migrations.Init())
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
srcRepo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
sess := loginUser(t, user.Name)
mirrorRepo, _, f := tests.CreateDeclarativeRepo(t, user, "", []unit.Type{unit.TypeCode}, nil, nil)
defer f()
ctx := NewAPITestContext(t, user.LowerName, srcRepo.Name)
ctx.Session = sess
remoteAddress := fmt.Sprintf("%s%s/%s", u.String(), url.PathEscape(user.Name), url.PathEscape(mirrorRepo.Name))
t.Run("Create push mirror with branch filter via web UI", func(t *testing.T) {
defer tests.PrintCurrentTest(t)()
doCreatePushMirrorWithBranchFilter(ctx, remoteAddress, user.LowerName, userPassword, "main,develop")(t)
mirrors, _, err := repo_model.GetPushMirrorsByRepoID(db.DefaultContext, srcRepo.ID, db.ListOptions{})
require.NoError(t, err)
require.Len(t, mirrors, 1)
assert.Equal(t, "main,develop", mirrors[0].BranchFilter)
assert.Equal(t, remoteAddress, mirrors[0].RemoteAddress)
})
t.Run("Create push mirror with empty branch filter via web UI", func(t *testing.T) {
defer tests.PrintCurrentTest(t)()
remoteAddress2 := fmt.Sprintf("%s%s/%s", u.String(), url.PathEscape(user.Name), url.PathEscape("foo"))
doCreatePushMirrorWithBranchFilter(ctx, remoteAddress2, user.LowerName, userPassword, "")(t)
mirrors, _, err := repo_model.GetPushMirrorsByRepoID(db.DefaultContext, srcRepo.ID, db.ListOptions{})
require.NoError(t, err)
require.Len(t, mirrors, 2)
var emptyMirror *repo_model.PushMirror
for _, mirror := range mirrors {
if mirror.RemoteAddress == remoteAddress2 {
emptyMirror = mirror
break
}
}
require.NotNil(t, emptyMirror)
assert.Empty(t, emptyMirror.BranchFilter)
})
t.Run("Verify branch filter field is visible in settings page", func(t *testing.T) {
defer tests.PrintCurrentTest(t)()
req := NewRequest(t, "GET", fmt.Sprintf("/%s/%s/settings", url.PathEscape(ctx.Username), url.PathEscape(ctx.Reponame)))
resp := sess.MakeRequest(t, req, http.StatusOK)
htmlDoc := NewHTMLParser(t, resp.Body)
htmlDoc.AssertElement(t, "input#push_mirror_branch_filter", true)
})
t.Run("Verify existing branch filter values are pre-populated", func(t *testing.T) {
defer tests.PrintCurrentTest(t)()
req := NewRequest(t, "GET", fmt.Sprintf("/%s/%s/settings", url.PathEscape(ctx.Username), url.PathEscape(ctx.Reponame)))
resp := sess.MakeRequest(t, req, http.StatusOK)
htmlDoc := NewHTMLParser(t, resp.Body)
// Find all edit buttons for push mirrors
editButtons := htmlDoc.Find("button[data-modal='#push-mirror-edit-modal']")
assert.Equal(t, 2, editButtons.Length(), "Should have 2 push mirror edit buttons")
// Get the created mirrors from database to match with UI elements
mirrors, _, err := repo_model.GetPushMirrorsByRepoID(db.DefaultContext, srcRepo.ID, db.ListOptions{})
require.NoError(t, err)
require.Len(t, mirrors, 2)
// Create a map of remote address to branch filter for easy lookup
expectedFilters := make(map[string]string)
for _, mirror := range mirrors {
expectedFilters[mirror.RemoteAddress] = mirror.BranchFilter
}
// Check each edit button has the correct branch filter data attribute
editButtons.Each(func(i int, s *goquery.Selection) {
remoteAddress, exists := s.Attr("data-modal-push-mirror-edit-address")
assert.True(t, exists, "Edit button should have remote address data attribute")
branchFilter, exists := s.Attr("data-modal-push-mirror-edit-branch-filter")
assert.True(t, exists, "Edit button should have branch filter data attribute")
expectedFilter, found := expectedFilters[remoteAddress]
assert.True(t, found, "Remote address should match one of the created mirrors")
assert.Equal(t, expectedFilter, branchFilter, "Branch filter should match the expected value for remote %s", remoteAddress)
})
// Verify the edit modal has the correct input field for branch filter editing
htmlDoc.AssertElement(t, "#push-mirror-edit-modal input[name='push_mirror_branch_filter']", true)
})
})
}
func TestPushMirrorBranchFilterIntegration(t *testing.T) {
onGiteaRun(t, func(t *testing.T, u *url.URL) {
defer test.MockVariableValue(&setting.Migrations.AllowLocalNetworks, true)()
defer test.MockVariableValue(&setting.Mirror.Enabled, true)()
require.NoError(t, migrations.Init())
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
srcRepo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
sess := loginUser(t, user.Name)
token := getTokenForLoggedInUser(t, sess, auth_model.AccessTokenScopeAll)
ctx := NewAPITestContext(t, user.LowerName, srcRepo.Name)
ctx.Session = sess
remoteAddress := fmt.Sprintf("%s%s/%s", u.String(), url.PathEscape(user.Name), url.PathEscape("foo"))
urlStr := fmt.Sprintf("/api/v1/repos/%s/%s/push_mirrors", user.LowerName, srcRepo.Name)
t.Run("Web UI to API integration", func(t *testing.T) {
defer tests.PrintCurrentTest(t)()
// Create push mirror with branch filter via web UI
doCreatePushMirrorWithBranchFilter(ctx, remoteAddress, user.LowerName, userPassword, "main,develop")(t)
// Verify it appears in API responses
req := NewRequest(t, "GET", urlStr).AddTokenAuth(token)
resp := MakeRequest(t, req, http.StatusOK)
var pushMirrors []*api.PushMirror
DecodeJSON(t, resp, &pushMirrors)
require.Len(t, pushMirrors, 1)
assert.Equal(t, "main,develop", pushMirrors[0].BranchFilter)
assert.Equal(t, remoteAddress, pushMirrors[0].RemoteAddress)
// Verify it's stored correctly in database
mirrors, _, err := repo_model.GetPushMirrorsByRepoID(db.DefaultContext, srcRepo.ID, db.ListOptions{})
require.NoError(t, err)
require.Len(t, mirrors, 1)
assert.Equal(t, "main,develop", mirrors[0].BranchFilter)
})
t.Run("API to Web UI integration", func(t *testing.T) {
defer tests.PrintCurrentTest(t)()
// Create another mirror repo for this test
mirrorRepo2, err := repo_service.CreateRepositoryDirectly(db.DefaultContext, user, user, repo_service.CreateRepoOptions{
Name: "test-api-to-ui",
})
require.NoError(t, err)
remoteAddress2 := fmt.Sprintf("%s%s/%s", u.String(), url.PathEscape(user.Name), url.PathEscape(mirrorRepo2.Name))
// Create push mirror with branch filter via API
req := NewRequestWithJSON(t, "POST", urlStr, &api.CreatePushMirrorOption{
RemoteAddress: remoteAddress2,
Interval: "8h",
BranchFilter: "feature-*,hotfix-*",
}).AddTokenAuth(token)
MakeRequest(t, req, http.StatusOK)
// Verify it's stored in database
mirrors, _, err := repo_model.GetPushMirrorsByRepoID(db.DefaultContext, srcRepo.ID, db.ListOptions{})
require.NoError(t, err)
require.Len(t, mirrors, 2) // Should have 2 mirrors now
// Find the mirror created via API
var apiMirror *repo_model.PushMirror
for _, mirror := range mirrors {
if mirror.RemoteAddress == remoteAddress2 {
apiMirror = mirror
break
}
}
require.NotNil(t, apiMirror)
assert.Equal(t, "feature-*,hotfix-*", apiMirror.BranchFilter)
// Verify it appears in web UI with correct branch filter data
req = NewRequest(t, "GET", fmt.Sprintf("/%s/%s/settings", url.PathEscape(ctx.Username), url.PathEscape(ctx.Reponame)))
resp := sess.MakeRequest(t, req, http.StatusOK)
assert.Equal(t, http.StatusOK, resp.Code)
// Check that the edit button has the correct data attributes for branch filter
doc := NewHTMLParser(t, resp.Body)
editButton := doc.Find(fmt.Sprintf(`button[data-modal-push-mirror-edit-address="%s"]`, remoteAddress2))
require.Equal(t, 1, editButton.Length(), "Should find exactly one edit button for the API-created mirror")
branchFilterAttr, exists := editButton.Attr("data-modal-push-mirror-edit-branch-filter")
require.True(t, exists, "Edit button should have branch filter data attribute")
assert.Equal(t, "feature-*,hotfix-*", branchFilterAttr, "Branch filter data attribute should match what was set via API")
})
})
}
func TestPushMirrorSettings(t *testing.T) {
onGiteaRun(t, func(t *testing.T, u *url.URL) {
defer test.MockVariableValue(&setting.Migrations.AllowLocalNetworks, true)()
@ -615,459 +400,3 @@ func TestPushMirrorSettings(t *testing.T) {
})
})
}
func TestPushMirrorBranchFilterSyncOperations(t *testing.T) {
onGiteaRun(t, func(t *testing.T, u *url.URL) {
defer test.MockVariableValue(&setting.Migrations.AllowLocalNetworks, true)()
defer test.MockVariableValue(&setting.Mirror.Enabled, true)()
require.NoError(t, migrations.Init())
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
srcRepo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
sess := loginUser(t, user.Name)
// Create test repository with multiple branches
testRepoPath := srcRepo.RepoPath()
// Create additional branches in source repository
_, _, err := git.NewCommand(git.DefaultContext, "update-ref", "refs/heads/develop", "refs/heads/master").RunStdString(&git.RunOpts{Dir: testRepoPath})
require.NoError(t, err)
_, _, err = git.NewCommand(git.DefaultContext, "update-ref", "refs/heads/feature-auth", "refs/heads/master").RunStdString(&git.RunOpts{Dir: testRepoPath})
require.NoError(t, err)
_, _, err = git.NewCommand(git.DefaultContext, "update-ref", "refs/heads/feature-ui", "refs/heads/master").RunStdString(&git.RunOpts{Dir: testRepoPath})
require.NoError(t, err)
_, _, err = git.NewCommand(git.DefaultContext, "update-ref", "refs/heads/hotfix-123", "refs/heads/master").RunStdString(&git.RunOpts{Dir: testRepoPath})
require.NoError(t, err)
ctx := NewAPITestContext(t, user.LowerName, srcRepo.Name)
ctx.Session = sess
t.Run("Create push mirror with branch filter and trigger sync", func(t *testing.T) {
defer tests.PrintCurrentTest(t)()
// Create mirror repository
mirrorRepo, err := repo_service.CreateRepositoryDirectly(db.DefaultContext, user, user, repo_service.CreateRepoOptions{
Name: "test-sync-branch-filter",
})
require.NoError(t, err)
remoteAddress := fmt.Sprintf("%s%s/%s", u.String(), url.PathEscape(user.Name), url.PathEscape(mirrorRepo.Name))
// Create push mirror with specific branch filter via web UI
branchFilter := "master,develop,feature-auth"
doCreatePushMirrorWithBranchFilter(ctx, remoteAddress, user.LowerName, userPassword, branchFilter)(t)
// Verify the push mirror was created with branch filter
mirrors, _, err := repo_model.GetPushMirrorsByRepoID(db.DefaultContext, srcRepo.ID, db.ListOptions{})
require.NoError(t, err)
require.Len(t, mirrors, 1)
assert.Equal(t, branchFilter, mirrors[0].BranchFilter)
// Verify git remote configuration includes correct refspecs for filtered branches
output, _, err := git.NewCommand(git.DefaultContext, "config", "--get-all").AddDynamicArguments(fmt.Sprintf("remote.%s.push", mirrors[0].RemoteName)).RunStdString(&git.RunOpts{Dir: testRepoPath})
require.NoError(t, err)
assert.Contains(t, output, "+refs/heads/master:refs/heads/master")
assert.Contains(t, output, "+refs/heads/develop:refs/heads/develop")
assert.Contains(t, output, "+refs/heads/feature-auth:refs/heads/feature-auth")
assert.NotContains(t, output, "+refs/heads/feature-ui:refs/heads/feature-ui")
assert.NotContains(t, output, "+refs/heads/hotfix-123:refs/heads/hotfix-123")
assert.Contains(t, output, "+refs/tags/*:refs/tags/*") // Tags should always be pushed
// Trigger sync operation
ok := mirror_service.SyncPushMirror(db.DefaultContext, mirrors[0].ID)
assert.True(t, ok)
// Verify only filtered branches were pushed to mirror
mirrorGitRepo, err := gitrepo.OpenRepository(git.DefaultContext, mirrorRepo)
require.NoError(t, err)
defer mirrorGitRepo.Close()
// Check that filtered branches exist in mirror
_, err = mirrorGitRepo.GetBranchCommit("master")
require.NoError(t, err, "master branch should exist in mirror")
_, err = mirrorGitRepo.GetBranchCommit("develop")
require.NoError(t, err, "develop branch should exist in mirror")
_, err = mirrorGitRepo.GetBranchCommit("feature-auth")
require.NoError(t, err, "feature-auth branch should exist in mirror")
// Check that non-filtered branches don't exist in mirror
_, err = mirrorGitRepo.GetBranchCommit("feature-ui")
require.Error(t, err, "feature-ui branch should not exist in mirror")
_, err = mirrorGitRepo.GetBranchCommit("hotfix-123")
require.Error(t, err, "hotfix-123 branch should not exist in mirror")
})
t.Run("Update branch filter and verify git remote settings are updated", func(t *testing.T) {
defer tests.PrintCurrentTest(t)()
// Get the existing mirror
mirrors, _, err := repo_model.GetPushMirrorsByRepoID(db.DefaultContext, srcRepo.ID, db.ListOptions{})
require.NoError(t, err)
require.Len(t, mirrors, 1)
mirror := mirrors[0]
mirror.Repo = srcRepo
// Update branch filter to include different branches
mirror.BranchFilter = "master,feature-ui,hotfix-123"
err = repo_model.UpdatePushMirror(db.DefaultContext, mirror)
require.NoError(t, err)
// Update git remote configuration
err = mirror_service.UpdatePushMirrorBranchFilter(db.DefaultContext, mirror)
require.NoError(t, err)
// Verify git remote configuration was updated
output, _, err := git.NewCommand(git.DefaultContext, "config", "--get-all").AddDynamicArguments(fmt.Sprintf("remote.%s.push", mirror.RemoteName)).RunStdString(&git.RunOpts{Dir: testRepoPath})
require.NoError(t, err)
assert.Contains(t, output, "+refs/heads/master:refs/heads/master")
assert.Contains(t, output, "+refs/heads/feature-ui:refs/heads/feature-ui")
assert.Contains(t, output, "+refs/heads/hotfix-123:refs/heads/hotfix-123")
assert.NotContains(t, output, "+refs/heads/develop:refs/heads/develop")
assert.NotContains(t, output, "+refs/heads/feature-auth:refs/heads/feature-auth")
assert.Contains(t, output, "+refs/tags/*:refs/tags/*") // Tags should always be pushed
// Trigger sync operation with updated filter
ok := mirror_service.SyncPushMirror(db.DefaultContext, mirror.ID)
assert.True(t, ok)
})
t.Run("Test empty branch filter pushes all branches", func(t *testing.T) {
defer tests.PrintCurrentTest(t)()
// Create another mirror repository for this test
mirrorRepo2, err := repo_service.CreateRepositoryDirectly(db.DefaultContext, user, user, repo_service.CreateRepoOptions{
Name: "test-sync-empty-filter",
})
require.NoError(t, err)
remoteAddress2 := fmt.Sprintf("%s%s/%s", u.String(), url.PathEscape(user.Name), url.PathEscape(mirrorRepo2.Name))
// Create push mirror with empty branch filter
doCreatePushMirrorWithBranchFilter(ctx, remoteAddress2, user.LowerName, userPassword, "")(t)
// Get the new mirror
mirrors, _, err := repo_model.GetPushMirrorsByRepoID(db.DefaultContext, srcRepo.ID, db.ListOptions{})
require.NoError(t, err)
require.Len(t, mirrors, 2) // Should have 2 mirrors now
var emptyFilterMirror *repo_model.PushMirror
for _, mirror := range mirrors {
if mirror.RemoteAddress == remoteAddress2 {
emptyFilterMirror = mirror
break
}
}
require.NotNil(t, emptyFilterMirror)
assert.Empty(t, emptyFilterMirror.BranchFilter)
// Verify git remote configuration for empty filter (should mirror all branches)
output, _, err := git.NewCommand(git.DefaultContext, "config", "--get-all").AddDynamicArguments(fmt.Sprintf("remote.%s.push", emptyFilterMirror.RemoteName)).RunStdString(&git.RunOpts{Dir: testRepoPath})
require.NoError(t, err)
assert.Contains(t, output, "+refs/heads/*:refs/heads/*") // Should mirror all branches
assert.Contains(t, output, "+refs/tags/*:refs/tags/*")
// Trigger sync operation
ok := mirror_service.SyncPushMirror(db.DefaultContext, emptyFilterMirror.ID)
assert.True(t, ok)
// Verify all branches were pushed to mirror
mirrorGitRepo2, err := gitrepo.OpenRepository(git.DefaultContext, mirrorRepo2)
require.NoError(t, err)
defer mirrorGitRepo2.Close()
// Check that all branches exist in mirror
_, err = mirrorGitRepo2.GetBranchCommit("master")
require.NoError(t, err, "master branch should exist in mirror")
_, err = mirrorGitRepo2.GetBranchCommit("develop")
require.NoError(t, err, "develop branch should exist in mirror")
_, err = mirrorGitRepo2.GetBranchCommit("feature-auth")
require.NoError(t, err, "feature-auth branch should exist in mirror")
_, err = mirrorGitRepo2.GetBranchCommit("feature-ui")
require.NoError(t, err, "feature-ui branch should exist in mirror")
_, err = mirrorGitRepo2.GetBranchCommit("hotfix-123")
require.NoError(t, err, "hotfix-123 branch should exist in mirror")
})
t.Run("Test glob pattern branch filter in sync operations", func(t *testing.T) {
defer tests.PrintCurrentTest(t)()
// Create another mirror repository for this test
mirrorRepo3, err := repo_service.CreateRepositoryDirectly(db.DefaultContext, user, user, repo_service.CreateRepoOptions{
Name: "test-sync-glob-filter",
})
require.NoError(t, err)
remoteAddress3 := fmt.Sprintf("%s%s/%s", u.String(), url.PathEscape(user.Name), url.PathEscape(mirrorRepo3.Name))
// Create push mirror with glob pattern branch filter
globFilter := "master,feature-*"
doCreatePushMirrorWithBranchFilter(ctx, remoteAddress3, user.LowerName, userPassword, globFilter)(t)
// Get the new mirror
mirrors, _, err := repo_model.GetPushMirrorsByRepoID(db.DefaultContext, srcRepo.ID, db.ListOptions{})
require.NoError(t, err)
require.Len(t, mirrors, 3) // Should have 3 mirrors now
var globMirror *repo_model.PushMirror
for _, mirror := range mirrors {
if mirror.RemoteAddress == remoteAddress3 {
globMirror = mirror
break
}
}
require.NotNil(t, globMirror)
assert.Equal(t, globFilter, globMirror.BranchFilter)
// Verify git remote configuration includes glob pattern branches
output, _, err := git.NewCommand(git.DefaultContext, "config", "--get-all").AddDynamicArguments(fmt.Sprintf("remote.%s.push", globMirror.RemoteName)).RunStdString(&git.RunOpts{Dir: testRepoPath})
require.NoError(t, err)
assert.Contains(t, output, "+refs/heads/master:refs/heads/master")
assert.Contains(t, output, "+refs/heads/feature-*:refs/heads/feature-*")
assert.NotContains(t, output, "+refs/heads/develop:refs/heads/develop")
assert.NotContains(t, output, "+refs/heads/hotfix-123:refs/heads/hotfix-123")
assert.Contains(t, output, "+refs/tags/*:refs/tags/*")
// Trigger sync operation
ok := mirror_service.SyncPushMirror(db.DefaultContext, globMirror.ID)
assert.True(t, ok)
// Verify only matching branches were pushed to mirror
mirrorGitRepo3, err := gitrepo.OpenRepository(git.DefaultContext, mirrorRepo3)
require.NoError(t, err)
defer mirrorGitRepo3.Close()
// Check that matching branches exist in mirror
_, err = mirrorGitRepo3.GetBranchCommit("master")
require.NoError(t, err, "master branch should exist in mirror")
_, err = mirrorGitRepo3.GetBranchCommit("feature-auth")
require.NoError(t, err, "feature-auth branch should exist in mirror")
_, err = mirrorGitRepo3.GetBranchCommit("feature-ui")
require.NoError(t, err, "feature-ui branch should exist in mirror")
// Check that non-matching branches don't exist in mirror
_, err = mirrorGitRepo3.GetBranchCommit("develop")
require.Error(t, err, "develop branch should not exist in mirror")
_, err = mirrorGitRepo3.GetBranchCommit("hotfix-123")
require.Error(t, err, "hotfix-123 branch should not exist in mirror")
})
})
}
func TestPushMirrorWebUIToAPIIntegration(t *testing.T) {
onGiteaRun(t, func(t *testing.T, u *url.URL) {
defer test.MockVariableValue(&setting.Migrations.AllowLocalNetworks, true)()
defer test.MockVariableValue(&setting.Mirror.Enabled, true)()
require.NoError(t, migrations.Init())
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
srcRepo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
session := loginUser(t, user.Name)
token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeAll)
mirrorRepo, _, f := tests.CreateDeclarativeRepo(t, user, "", []unit.Type{unit.TypeCode}, nil, nil)
defer f()
ctx := NewAPITestContext(t, user.LowerName, srcRepo.Name)
ctx.Session = session
remoteAddress := fmt.Sprintf("%s%s/%s", u.String(), url.PathEscape(user.Name), url.PathEscape(mirrorRepo.Name))
urlStr := fmt.Sprintf("/api/v1/repos/%s/%s/push_mirrors", user.Name, srcRepo.Name)
t.Run("Set branch filter via web UI and verify in API response", func(t *testing.T) {
defer tests.PrintCurrentTest(t)()
// Create push mirror with branch filter via web UI
branchFilter := "main,develop,feature-*"
doCreatePushMirrorWithBranchFilter(ctx, remoteAddress, user.LowerName, userPassword, branchFilter)(t)
// Verify via API that branch filter is set correctly
req := NewRequest(t, "GET", urlStr).AddTokenAuth(token)
resp := MakeRequest(t, req, http.StatusOK)
var pushMirrors []*api.PushMirror
DecodeJSON(t, resp, &pushMirrors)
require.Len(t, pushMirrors, 1)
assert.Equal(t, branchFilter, pushMirrors[0].BranchFilter, "Branch filter set via web UI should appear in API response")
assert.Equal(t, remoteAddress, pushMirrors[0].RemoteAddress)
// Store mirror info for cleanup
mirrorRemoteName := pushMirrors[0].RemoteName
// Cleanup
req = NewRequest(t, "DELETE", fmt.Sprintf("%s/%s", urlStr, mirrorRemoteName)).AddTokenAuth(token)
MakeRequest(t, req, http.StatusNoContent)
})
t.Run("Set empty branch filter via web UI and verify in API response", func(t *testing.T) {
defer tests.PrintCurrentTest(t)()
// Create another mirror repo for this test
mirrorRepo2, _, f := tests.CreateDeclarativeRepo(t, user, "", []unit.Type{unit.TypeCode}, nil, nil)
defer f()
remoteAddress2 := fmt.Sprintf("%s%s/%s", u.String(), url.PathEscape(user.Name), url.PathEscape(mirrorRepo2.Name))
// Create push mirror with empty branch filter via web UI
doCreatePushMirrorWithBranchFilter(ctx, remoteAddress2, user.LowerName, userPassword, "")(t)
// Verify via API that branch filter is empty
req := NewRequest(t, "GET", urlStr).AddTokenAuth(token)
resp := MakeRequest(t, req, http.StatusOK)
var pushMirrors []*api.PushMirror
DecodeJSON(t, resp, &pushMirrors)
require.Len(t, pushMirrors, 1)
assert.Empty(t, pushMirrors[0].BranchFilter, "Empty branch filter set via web UI should appear as empty in API response")
assert.Equal(t, remoteAddress2, pushMirrors[0].RemoteAddress)
// Cleanup
req = NewRequest(t, "DELETE", fmt.Sprintf("%s/%s", urlStr, pushMirrors[0].RemoteName)).AddTokenAuth(token)
MakeRequest(t, req, http.StatusNoContent)
})
t.Run("Set complex branch filter via web UI and verify in API response", func(t *testing.T) {
defer tests.PrintCurrentTest(t)()
// Create another mirror repo for this test
mirrorRepo3, _, f := tests.CreateDeclarativeRepo(t, user, "", []unit.Type{unit.TypeCode}, nil, nil)
defer f()
remoteAddress3 := fmt.Sprintf("%s%s/%s", u.String(), url.PathEscape(user.Name), url.PathEscape(mirrorRepo3.Name))
// Create push mirror with complex branch filter via web UI
complexFilter := "main,release/v*,hotfix-*,feature-auth,feature-ui"
doCreatePushMirrorWithBranchFilter(ctx, remoteAddress3, user.LowerName, userPassword, complexFilter)(t)
// Verify via API that complex branch filter is preserved
req := NewRequest(t, "GET", urlStr).AddTokenAuth(token)
resp := MakeRequest(t, req, http.StatusOK)
var pushMirrors []*api.PushMirror
DecodeJSON(t, resp, &pushMirrors)
require.Len(t, pushMirrors, 1)
assert.Equal(t, complexFilter, pushMirrors[0].BranchFilter, "Complex branch filter set via web UI should be preserved in API response")
assert.Equal(t, remoteAddress3, pushMirrors[0].RemoteAddress)
// Cleanup
req = NewRequest(t, "DELETE", fmt.Sprintf("%s/%s", urlStr, pushMirrors[0].RemoteName)).AddTokenAuth(token)
MakeRequest(t, req, http.StatusNoContent)
})
t.Run("Update branch filter via API and verify in web UI", func(t *testing.T) {
defer tests.PrintCurrentTest(t)()
// Create another mirror repo for this test
mirrorRepo4, _, f := tests.CreateDeclarativeRepo(t, user, "", []unit.Type{unit.TypeCode}, nil, nil)
defer f()
remoteAddress4 := fmt.Sprintf("%s%s/%s", u.String(), url.PathEscape(user.Name), url.PathEscape(mirrorRepo4.Name))
// First create a push mirror via API with initial branch filter
initialFilter := "main"
req := NewRequestWithJSON(t, "POST", urlStr, &api.CreatePushMirrorOption{
RemoteAddress: remoteAddress4,
Interval: "8h",
BranchFilter: initialFilter,
}).AddTokenAuth(token)
MakeRequest(t, req, http.StatusOK)
// Get the created mirror info
req = NewRequest(t, "GET", urlStr).AddTokenAuth(token)
resp := MakeRequest(t, req, http.StatusOK)
var pushMirrors []*api.PushMirror
DecodeJSON(t, resp, &pushMirrors)
require.Len(t, pushMirrors, 1)
assert.Equal(t, initialFilter, pushMirrors[0].BranchFilter)
mirrorRemoteName := pushMirrors[0].RemoteName
// Get the actual mirror from database to get the ID
dbMirrors, _, err := repo_model.GetPushMirrorsByRepoID(db.DefaultContext, srcRepo.ID, db.ListOptions{})
require.NoError(t, err)
require.Len(t, dbMirrors, 1)
// Update branch filter via web form (using existing repo settings endpoint)
updatedFilter := "main,develop,feature-*"
req = NewRequestWithValues(t, "POST", fmt.Sprintf("/%s/%s/settings", url.PathEscape(user.Name), url.PathEscape(srcRepo.Name)), map[string]string{
"_csrf": GetCSRF(t, session, fmt.Sprintf("/%s/%s/settings", url.PathEscape(user.Name), url.PathEscape(srcRepo.Name))),
"action": "push-mirror-update",
"push_mirror_id": fmt.Sprintf("%d", dbMirrors[0].ID),
"push_mirror_interval": "8h",
"push_mirror_branch_filter": updatedFilter,
})
session.MakeRequest(t, req, http.StatusSeeOther)
// Verify the branch filter was updated via API
req = NewRequest(t, "GET", urlStr).AddTokenAuth(token)
resp = MakeRequest(t, req, http.StatusOK)
DecodeJSON(t, resp, &pushMirrors)
require.Len(t, pushMirrors, 1)
assert.Equal(t, updatedFilter, pushMirrors[0].BranchFilter, "Branch filter should be updated via web form")
// Verify the branch filter is visible in the web UI settings page
req = NewRequest(t, "GET", fmt.Sprintf("/%s/%s/settings", url.PathEscape(user.Name), url.PathEscape(srcRepo.Name)))
resp = session.MakeRequest(t, req, http.StatusOK)
htmlDoc := NewHTMLParser(t, resp.Body)
editButton := htmlDoc.Find(fmt.Sprintf(`button[data-modal-push-mirror-edit-address="%s"]`, remoteAddress4))
require.Equal(t, 1, editButton.Length(), "Should find exactly one edit button for the updated mirror")
branchFilterAttr, exists := editButton.Attr("data-modal-push-mirror-edit-branch-filter")
require.True(t, exists, "Edit button should have branch filter data attribute")
assert.Equal(t, updatedFilter, branchFilterAttr, "Branch filter data attribute should match the updated value")
// Cleanup
req = NewRequest(t, "DELETE", fmt.Sprintf("%s/%s", urlStr, mirrorRemoteName)).AddTokenAuth(token)
MakeRequest(t, req, http.StatusNoContent)
})
t.Run("Multiple mirrors with different branch filters - UI to API consistency", func(t *testing.T) {
defer tests.PrintCurrentTest(t)()
// Create multiple mirror repos
testCases := []struct {
name string
branchFilter string
}{
{"multi-test-1", "main,develop"},
{"multi-test-2", "feature-*,hotfix-*"},
{"multi-test-3", ""},
}
for _, tc := range testCases {
remoteAddress := fmt.Sprintf("%s%s/%s", u.String(), url.PathEscape(user.Name), url.PathEscape(tc.name))
req := NewRequestWithJSON(t, "POST", urlStr, &api.CreatePushMirrorOption{
RemoteAddress: remoteAddress,
Interval: "8h",
BranchFilter: tc.branchFilter,
}).AddTokenAuth(token)
MakeRequest(t, req, http.StatusOK)
}
// Verify all mirrors and their branch filters via API
req := NewRequest(t, "GET", urlStr).AddTokenAuth(token)
resp := MakeRequest(t, req, http.StatusOK)
var pushMirrors []*api.PushMirror
DecodeJSON(t, resp, &pushMirrors)
require.Len(t, pushMirrors, 3)
// Create a map for easier verification
filterMap := make(map[string]string)
for _, mirror := range pushMirrors {
for _, tc := range testCases {
if strings.Contains(mirror.RemoteAddress, tc.name) {
filterMap[tc.name] = mirror.BranchFilter
// createdMirrors = append(createdMirrors, mirror.RemoteName)
break
}
}
}
// Verify each branch filter is correctly preserved
assert.Equal(t, "main,develop", filterMap["multi-test-1"], "First mirror branch filter should match")
assert.Equal(t, "feature-*,hotfix-*", filterMap["multi-test-2"], "Second mirror branch filter should match")
assert.Empty(t, filterMap["multi-test-3"], "Third mirror branch filter should be empty")
})
t.Run("Verify branch filter field exists in web UI form", func(t *testing.T) {
defer tests.PrintCurrentTest(t)()
// Access the push mirror settings page
req := NewRequest(t, "GET", fmt.Sprintf("/%s/%s/settings", url.PathEscape(user.Name), url.PathEscape(srcRepo.Name)))
resp := session.MakeRequest(t, req, http.StatusOK)
htmlDoc := NewHTMLParser(t, resp.Body)
htmlDoc.AssertElement(t, "#push_mirror_branch_filter", true)
})
})
}