fix: classify git failures on stderr, restrict migration failure detail (#39010)

Signed-off-by: silverwind <me@silverwind.io>
Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
This commit is contained in:
silverwind
2026-09-15 10:59:34 +02:00
committed by GitHub
co-authored by wxiaoguang
parent 7b036e96c2
commit 812191c0f9
30 changed files with 136 additions and 137 deletions
-17
View File
@@ -124,20 +124,3 @@ func (err *ErrPushRejected) GenerateMessage() {
} }
err.Message = strings.TrimSpace(messageBuilder.String()) err.Message = strings.TrimSpace(messageBuilder.String())
} }
// ErrMoreThanOne represents an error if pull request fails when there are more than one sources (branch, tag) with the same name
type ErrMoreThanOne struct {
StdOut string
StdErr string
Err error
}
// IsErrMoreThanOne checks if an error is a ErrMoreThanOne
func IsErrMoreThanOne(err error) bool {
_, ok := err.(*ErrMoreThanOne)
return ok
}
func (err *ErrMoreThanOne) Error() string {
return fmt.Sprintf("ErrMoreThanOne Error: %v: %s\n%s", err.Err, err.StdErr, err.StdOut)
}
+1 -1
View File
@@ -539,7 +539,7 @@ func (c *Command) WaitWithStderr() RunStdError {
// if no exec error but only stderr output, the stderr output is still saved in "c.cmdManagedStderr" and can be read later // if no exec error but only stderr output, the stderr output is still saved in "c.cmdManagedStderr" and can be read later
return nil return nil
} }
return &runStdError{err: errWait, stderr: util.UnsafeBytesToString(c.cmdManagedStderr.Bytes())} return NewRunStdError(errWait, util.UnsafeBytesToString(c.cmdManagedStderr.Bytes()))
} }
func (c *Command) RunWithStderr(ctx context.Context) RunStdError { func (c *Command) RunWithStderr(ctx context.Context) RunStdError {
+12 -3
View File
@@ -44,6 +44,10 @@ func (r *runStdError) Stderr() string {
return r.stderr return r.stderr
} }
func NewRunStdError(err error, stderr string) RunStdError {
return &runStdError{err: err, stderr: util.NormalizeStringEOL(stderr)}
}
func ErrorAsStderr(err error) (string, bool) { func ErrorAsStderr(err error) (string, bool) {
if runErr, ok := errors.AsType[RunStdError](err); ok { if runErr, ok := errors.AsType[RunStdError](err); ok {
return runErr.Stderr(), true return runErr.Stderr(), true
@@ -93,6 +97,9 @@ const (
StderrNoSuchRemote1 StderrPrefix = "fatal: no such remote" // git < 2.30, exit status 128 StderrNoSuchRemote1 StderrPrefix = "fatal: no such remote" // git < 2.30, exit status 128
StderrNoSuchRemote2 StderrPrefix = "error: no such remote" // git >= 2.30. exit status 2 StderrNoSuchRemote2 StderrPrefix = "error: no such remote" // git >= 2.30. exit status 2
StderrAuthenticationFailed StderrPrefix = "fatal: Authentication failed for"
StderrCouldNotReadUsername StderrPrefix = "fatal: could not read Username"
StderrUnknownRevisionOrPath StderrRegexp = "^fatal: .*: unknown revision or path not in the working tree" StderrUnknownRevisionOrPath StderrRegexp = "^fatal: .*: unknown revision or path not in the working tree"
StderrNoMergeBase StderrRegexp = "^fatal: .*: no merge base" StderrNoMergeBase StderrRegexp = "^fatal: .*: no merge base"
StderrFileNoEnoughLines StderrRegexp = `^fatal: file .* has only \d+ lines?` StderrFileNoEnoughLines StderrRegexp = `^fatal: file .* has only \d+ lines?`
@@ -126,9 +133,11 @@ func IsStderr(err error, checks ...StderrCheck) bool {
return false return false
} }
for _, checkIntf := range checks { for line := range strings.SplitSeq(stderr, "\n") { // git can emit multiple-line message in stderr
if matchStderrCheck(stderr, checkIntf) { for _, checkIntf := range checks {
return true if matchStderrCheck(line, checkIntf) {
return true
}
} }
} }
return false return false
+2
View File
@@ -17,8 +17,10 @@ func TestIsStderr(t *testing.T) {
{StderrUnknownRevisionOrPath, "fatal: ambiguous argument 'origin': unknown revision or path not in the working tree...."}, {StderrUnknownRevisionOrPath, "fatal: ambiguous argument 'origin': unknown revision or path not in the working tree...."},
{StderrNoMergeBase, "fatal: origin/main..HEAD: no merge base...."}, {StderrNoMergeBase, "fatal: origin/main..HEAD: no merge base...."},
{StderrFileNoEnoughLines, "fatal: file foo/bar has only 1 line"}, {StderrFileNoEnoughLines, "fatal: file foo/bar has only 1 line"},
{StderrAuthenticationFailed, "Cloning into 'repo'...\r\nremote: Invalid username or token.\r\nfatal: Authentication failed for 'https://host/repo.git/'\r\n"},
} }
for _, tc := range cases { for _, tc := range cases {
assert.True(t, IsStderr(&runStdError{stderr: tc.stderr}, tc.check), "stderr: %s", tc.stderr) assert.True(t, IsStderr(&runStdError{stderr: tc.stderr}, tc.check), "stderr: %s", tc.stderr)
} }
assert.False(t, IsStderr(&runStdError{stderr: "remote: fatal: Authentication failed for 'https://host/repo.git/'\n"}, StderrAuthenticationFailed))
} }
+1 -1
View File
@@ -66,7 +66,7 @@ func (err *ErrInvalidCloneAddr) Unwrap() error {
// IsRemoteNotExistError checks the prefix of the error message to see whether a remote does not exist. // IsRemoteNotExistError checks the prefix of the error message to see whether a remote does not exist.
func IsRemoteNotExistError(err error) bool { func IsRemoteNotExistError(err error) bool {
return gitcmd.IsStderr(err, gitcmd.StderrNoSuchRemote1) || gitcmd.IsStderr(err, gitcmd.StderrNoSuchRemote2) return gitcmd.IsStderr(err, gitcmd.StderrNoSuchRemote1, gitcmd.StderrNoSuchRemote2)
} }
// ParseRemoteAddr checks if given remote address is valid, // ParseRemoteAddr checks if given remote address is valid,
+3 -3
View File
@@ -47,11 +47,11 @@ func ManagedRemoteRemove(ctx context.Context, repo RepositoryFacade, remoteName
func ParseRemoteAddressURL(ctx context.Context, repo RepositoryFacade, remoteName string) (*giturl.GitURL, error) { func ParseRemoteAddressURL(ctx context.Context, repo RepositoryFacade, remoteName string) (*giturl.GitURL, error) {
addr, err := GetRemoteAddress(ctx, repo, remoteName) addr, err := GetRemoteAddress(ctx, repo, remoteName)
if (addr == "" && err == nil) || IsRemoteNotExistError(err) {
return nil, util.NewNotExistErrorf("remote '%s' does not exist", remoteName)
}
if err != nil { if err != nil {
return nil, err return nil, err
} }
if addr == "" {
return nil, util.NewNotExistErrorf("remote '%s' does not exist", remoteName)
}
return giturl.ParseGitURL(addr) return giturl.ParseGitURL(addr)
} }
-2
View File
@@ -279,8 +279,6 @@ func Push(ctx context.Context, localRepoPath string, opts PushOptions) error {
err := &ErrPushRejected{StdOut: stdout, StdErr: stderr, Err: err} err := &ErrPushRejected{StdOut: stdout, StdErr: stderr, Err: err}
err.GenerateMessage() err.GenerateMessage()
return err return err
} else if strings.Contains(stderr, "matches more than one") {
return &ErrMoreThanOne{StdOut: stdout, StdErr: stderr, Err: err}
} }
return fmt.Errorf("push failed: %w - %s\n%s", err, stderr, stdout) return fmt.Errorf("push failed: %w - %s\n%s", err, stderr, stdout)
} }
-3
View File
@@ -571,8 +571,6 @@
"form.repository_files_already_exist.adopt": "Files already exist for this repository and can only be adopted.", "form.repository_files_already_exist.adopt": "Files already exist for this repository and can only be adopted.",
"form.repository_files_already_exist.delete": "Files already exist for this repository. You must delete them.", "form.repository_files_already_exist.delete": "Files already exist for this repository. You must delete them.",
"form.repository_files_already_exist.adopt_or_delete": "Files already exist for this repository. Either adopt them or delete them.", "form.repository_files_already_exist.adopt_or_delete": "Files already exist for this repository. Either adopt them or delete them.",
"form.visit_rate_limit": "Remote visit addressed rate limitation.",
"form.2fa_auth_required": "Remote visit required two-factor authentication.",
"form.org_name_been_taken": "The organization name is already taken.", "form.org_name_been_taken": "The organization name is already taken.",
"form.team_name_been_taken": "The team name is already taken.", "form.team_name_been_taken": "The team name is already taken.",
"form.team_no_units_error": "Allow access to at least one repository section.", "form.team_no_units_error": "Allow access to at least one repository section.",
@@ -603,7 +601,6 @@
"form.invalid_ssh_principal": "Invalid principal: %s", "form.invalid_ssh_principal": "Invalid principal: %s",
"form.must_use_public_key": "The key you provided is a private key. Please do not upload your private key anywhere. Use your public key instead.", "form.must_use_public_key": "The key you provided is a private key. Please do not upload your private key anywhere. Use your public key instead.",
"form.unable_verify_ssh_key": "Cannot verify the SSH key. Double-check it for mistakes.", "form.unable_verify_ssh_key": "Cannot verify the SSH key. Double-check it for mistakes.",
"form.auth_failed": "Authentication failed: %v",
"form.still_own_repo": "Your account owns one or more repositories. Delete or transfer them first.", "form.still_own_repo": "Your account owns one or more repositories. Delete or transfer them first.",
"form.still_has_org": "Your account is a member of one or more organizations. Leave them first.", "form.still_has_org": "Your account is a member of one or more organizations. Leave them first.",
"form.still_own_packages": "Your account owns one or more packages. Delete them first.", "form.still_own_packages": "Your account owns one or more packages. Delete them first.",
+3 -5
View File
@@ -8,7 +8,6 @@ import (
"errors" "errors"
"fmt" "fmt"
"net/http" "net/http"
"strings"
"gitea.dev/models/db" "gitea.dev/models/db"
"gitea.dev/models/organization" "gitea.dev/models/organization"
@@ -17,6 +16,7 @@ import (
repo_model "gitea.dev/models/repo" repo_model "gitea.dev/models/repo"
user_model "gitea.dev/models/user" user_model "gitea.dev/models/user"
"gitea.dev/modules/git" "gitea.dev/modules/git"
"gitea.dev/modules/git/gitcmd"
"gitea.dev/modules/graceful" "gitea.dev/modules/graceful"
"gitea.dev/modules/lfs" "gitea.dev/modules/lfs"
"gitea.dev/modules/log" "gitea.dev/modules/log"
@@ -245,11 +245,9 @@ func handleMigrateError(ctx *context.APIContext, repoOwner *user_model.User, err
ctx.APIError(http.StatusUnprocessableEntity, err.Error()) ctx.APIError(http.StatusUnprocessableEntity, err.Error())
default: default:
err = util.SanitizeErrorCredentialURLs(err) err = util.SanitizeErrorCredentialURLs(err)
if strings.Contains(err.Error(), "Authentication failed") || if migrations.IsAuthenticationError(err) {
strings.Contains(err.Error(), "Bad credentials") ||
strings.Contains(err.Error(), "could not read Username") {
ctx.APIError(http.StatusUnprocessableEntity, fmt.Sprintf("Authentication failed: %v.", err)) ctx.APIError(http.StatusUnprocessableEntity, fmt.Sprintf("Authentication failed: %v.", err))
} else if strings.Contains(err.Error(), "fatal:") { } else if _, ok := gitcmd.ErrorAsStderr(err); ok {
ctx.APIError(http.StatusUnprocessableEntity, fmt.Sprintf("Migration failed: %v.", err)) ctx.APIError(http.StatusUnprocessableEntity, fmt.Sprintf("Migration failed: %v.", err))
} else { } else {
ctx.APIErrorInternal(err) ctx.APIErrorInternal(err)
+1 -1
View File
@@ -624,7 +624,7 @@ func prepareIssueFilterAndList(ctx *context.Context, milestoneID int64, projectI
showArchivedLabels := ctx.FormBool("archived_labels") showArchivedLabels := ctx.FormBool("archived_labels")
ctx.Data["ShowArchivedLabels"] = showArchivedLabels ctx.Data["ShowArchivedLabels"] = showArchivedLabels
ctx.Data["PinnedIssues"] = pinned ctx.Data["PinnedIssues"] = pinned
ctx.Data["IsRepoAdmin"] = ctx.IsSigned && (ctx.Repo.Permission.IsAdmin() || ctx.Doer.IsAdmin) ctx.Data["IsRepoAdmin"] = ctx.Repo.Permission.IsAdmin()
ctx.Data["IssueStats"] = issueStats ctx.Data["IssueStats"] = issueStats
ctx.Data["OpenCount"] = issueStats.OpenCount ctx.Data["OpenCount"] = issueStats.OpenCount
ctx.Data["ClosedCount"] = issueStats.ClosedCount ctx.Data["ClosedCount"] = issueStats.ClosedCount
+2 -2
View File
@@ -399,7 +399,7 @@ func ViewIssue(ctx *context.Context) {
ctx.Data["IsIssuePoster"] = ctx.IsSigned && issue.IsPoster(ctx.Doer.ID) ctx.Data["IsIssuePoster"] = ctx.IsSigned && issue.IsPoster(ctx.Doer.ID)
ctx.Data["HasIssuesOrPullsWritePermission"] = ctx.Repo.Permission.CanWriteIssuesOrPulls(issue.IsPull) ctx.Data["HasIssuesOrPullsWritePermission"] = ctx.Repo.Permission.CanWriteIssuesOrPulls(issue.IsPull)
ctx.Data["HasProjectsWritePermission"] = ctx.Repo.Permission.CanWrite(unit.TypeProjects) ctx.Data["HasProjectsWritePermission"] = ctx.Repo.Permission.CanWrite(unit.TypeProjects)
ctx.Data["IsRepoAdmin"] = ctx.IsSigned && (ctx.Repo.Permission.IsAdmin() || ctx.Doer.IsAdmin) ctx.Data["IsRepoAdmin"] = ctx.Repo.Permission.IsAdmin()
ctx.Data["LockReasons"] = setting.Repository.Issue.LockReasons ctx.Data["LockReasons"] = setting.Repository.Issue.LockReasons
ctx.Data["RefEndName"] = git.RefName(issue.Ref).ShortName() ctx.Data["RefEndName"] = git.RefName(issue.Ref).ShortName()
@@ -937,7 +937,7 @@ func (prInfo *pullRequestViewInfo) prepareMergeBox(ctx *context.Context, issue *
// Otherwise, there is nothing to do, because the PR view page already contains enough information. // Otherwise, there is nothing to do, because the PR view page already contains enough information.
data.ShowMergeBox = !pull.HasMerged || data.IsPullBranchDeletable data.ShowMergeBox = !pull.HasMerged || data.IsPullBranchDeletable
isRepoAdmin := ctx.IsSigned && (ctx.Repo.Permission.IsAdmin() || ctx.Doer.IsAdmin) isRepoAdmin := ctx.Repo.Permission.IsAdmin()
// admin can merge without checks, writer can merge when checks succeed // admin can merge without checks, writer can merge when checks succeed
// admin and writer both can make an auto merge schedule (not affected by overridable blockers) // admin and writer both can make an auto merge schedule (not affected by overridable blockers)
+7 -12
View File
@@ -8,13 +8,13 @@ import (
"errors" "errors"
"net/http" "net/http"
"net/url" "net/url"
"strings"
admin_model "gitea.dev/models/admin" admin_model "gitea.dev/models/admin"
"gitea.dev/models/db" "gitea.dev/models/db"
repo_model "gitea.dev/models/repo" repo_model "gitea.dev/models/repo"
user_model "gitea.dev/models/user" user_model "gitea.dev/models/user"
"gitea.dev/modules/git" "gitea.dev/modules/git"
"gitea.dev/modules/git/gitcmd"
"gitea.dev/modules/json" "gitea.dev/modules/json"
"gitea.dev/modules/lfs" "gitea.dev/modules/lfs"
"gitea.dev/modules/log" "gitea.dev/modules/log"
@@ -81,10 +81,6 @@ func handleMigrateError(ctx *context.Context, owner *user_model.User, err error,
var errNameReserved db.ErrNameReserved var errNameReserved db.ErrNameReserved
var errNamePatternNotAllowed db.ErrNamePatternNotAllowed var errNamePatternNotAllowed db.ErrNamePatternNotAllowed
switch { switch {
case migrations.IsRateLimitError(err):
ctx.RenderWithErrDeprecated(ctx.Tr("form.visit_rate_limit"), tpl, form)
case migrations.IsTwoFactorAuthError(err):
ctx.RenderWithErrDeprecated(ctx.Tr("form.2fa_auth_required"), tpl, form)
case repo_model.IsErrReachLimitOfRepo(err): case repo_model.IsErrReachLimitOfRepo(err):
maxCreationLimit := owner.MaxCreationLimit() maxCreationLimit := owner.MaxCreationLimit()
msg := ctx.TrN(maxCreationLimit, "repo.form.reach_limit_of_creation_1", "repo.form.reach_limit_of_creation_n", maxCreationLimit) msg := ctx.TrN(maxCreationLimit, "repo.form.reach_limit_of_creation_1", "repo.form.reach_limit_of_creation_n", maxCreationLimit)
@@ -112,12 +108,7 @@ func handleMigrateError(ctx *context.Context, owner *user_model.User, err error,
ctx.RenderWithErrDeprecated(ctx.Tr("repo.form.name_pattern_not_allowed", errNamePatternNotAllowed.Pattern), tpl, form) ctx.RenderWithErrDeprecated(ctx.Tr("repo.form.name_pattern_not_allowed", errNamePatternNotAllowed.Pattern), tpl, form)
default: default:
err = util.SanitizeErrorCredentialURLs(err) err = util.SanitizeErrorCredentialURLs(err)
if strings.Contains(err.Error(), "Authentication failed") || if _, fromGit := gitcmd.ErrorAsStderr(err); fromGit {
strings.Contains(err.Error(), "Bad credentials") ||
strings.Contains(err.Error(), "could not read Username") {
ctx.Data["Err_Auth"] = true
ctx.RenderWithErrDeprecated(ctx.Tr("form.auth_failed", err.Error()), tpl, form)
} else if strings.Contains(err.Error(), "fatal:") {
ctx.Data["Err_CloneAddr"] = true ctx.Data["Err_CloneAddr"] = true
ctx.RenderWithErrDeprecated(ctx.Tr("repo.migrate.failed", err.Error()), tpl, form) ctx.RenderWithErrDeprecated(ctx.Tr("repo.migrate.failed", err.Error()), tpl, form)
} else { } else {
@@ -312,7 +303,11 @@ func MigrateStatus(ctx *context.Context) {
message := task.Message message := task.Message
if task.Message != "" && task.Message[0] == '{' { // a failure message can echo bytes the remote chose, so only whoever started the migration may read it
canSeeFailure := (ctx.Doer != nil && ctx.Doer.ID == task.DoerID) || ctx.Repo.Permission.IsAdmin()
if task.Status == structs.TaskStatusFailed && !canSeeFailure {
message = ctx.Locale.TrString("repo.migrate.migrating_failed_no_addr")
} else if message != "" && message[0] == '{' {
// assume message is actually a translatable string // assume message is actually a translatable string
var translatableMessage admin_model.TranslatableMessage var translatableMessage admin_model.TranslatableMessage
if err := json.Unmarshal([]byte(message), &translatableMessage); err != nil { if err := json.Unmarshal([]byte(message), &translatableMessage); err != nil {
+12
View File
@@ -6,6 +6,9 @@ package migrations
import ( import (
"errors" "errors"
"net/http"
"gitea.dev/modules/git/gitcmd"
"github.com/google/go-github/v91/github" "github.com/google/go-github/v91/github"
) )
@@ -24,3 +27,12 @@ func IsTwoFactorAuthError(err error) bool {
_, ok := err.(*github.TwoFactorAuthError) _, ok := err.(*github.TwoFactorAuthError)
return ok return ok
} }
// IsAuthenticationError returns true if the remote rejected the credentials, over git or over its HTTP API
func IsAuthenticationError(err error) bool {
if gitcmd.IsStderr(err, gitcmd.StderrAuthenticationFailed, gitcmd.StderrCouldNotReadUsername) {
return true
}
githubErr, ok := errors.AsType[*github.ErrorResponse](err)
return ok && githubErr.Response != nil && githubErr.Response.StatusCode == http.StatusUnauthorized
}
+2 -1
View File
@@ -130,7 +130,8 @@ func MigrateRepository(ctx context.Context, doer *user_model.User, ownerName str
if err1 := uploader.Rollback(); err1 != nil { if err1 := uploader.Rollback(); err1 != nil {
log.Error("rollback failed: %v", err1) log.Error("rollback failed: %v", err1)
} }
if err2 := system_model.CreateRepositoryNotice(fmt.Sprintf("Migrate repository (%s/%s) from %s failed: %v", ownerName, opts.RepoName, opts.OriginalURL, err)); err2 != nil { noticeMsg := fmt.Sprintf("Migrate repository (%s/%s) from %s failed: %v", ownerName, opts.RepoName, util.SanitizeCredentialURLs(opts.OriginalURL), util.SanitizeErrorCredentialURLs(err))
if err2 := system_model.CreateRepositoryNotice(noticeMsg); err2 != nil {
log.Error("create repository notice failed: ", err2) log.Error("create repository notice failed: ", err2)
} }
return nil, err return nil, err
+27
View File
@@ -4,17 +4,44 @@
package migrations package migrations
import ( import (
"errors"
"fmt"
"net" "net"
"net/http"
"path/filepath" "path/filepath"
"testing" "testing"
"gitea.dev/models/unittest" "gitea.dev/models/unittest"
user_model "gitea.dev/models/user" user_model "gitea.dev/models/user"
"gitea.dev/modules/git/gitcmd"
"gitea.dev/modules/setting" "gitea.dev/modules/setting"
"gitea.dev/modules/util"
"github.com/google/go-github/v91/github"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
) )
func TestIsAuthenticationError(t *testing.T) {
errDummy := errors.New("dummy")
cases := []struct {
name string
want bool
err error
}{
{"git authentication failed", true, gitcmd.NewRunStdError(errDummy, "fatal: Authentication failed for 'https://host/repo.git/'")},
{"git could not read username", true, fmt.Errorf("%w", gitcmd.NewRunStdError(errDummy, "fatal: could not read Username for 'https://host'"))},
{"github unauthorized", true, util.SanitizeErrorCredentialURLs(&github.ErrorResponse{Response: &http.Response{StatusCode: http.StatusUnauthorized}})},
{"github other", false, &github.ErrorResponse{Response: &http.Response{StatusCode: http.StatusNotFound}}},
{"github nil response", false, &github.ErrorResponse{}},
{"unrelated error", false, errDummy},
}
for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
assert.Equal(t, c.want, IsAuthenticationError(c.err))
})
}
}
func TestMigrateWhiteBlocklist(t *testing.T) { func TestMigrateWhiteBlocklist(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase()) assert.NoError(t, unittest.PrepareTestDatabase())
+21 -25
View File
@@ -14,6 +14,7 @@ import (
"gitea.dev/models/db" "gitea.dev/models/db"
repo_model "gitea.dev/models/repo" repo_model "gitea.dev/models/repo"
"gitea.dev/modules/git" "gitea.dev/modules/git"
"gitea.dev/modules/git/gitrepo"
"gitea.dev/modules/lfs" "gitea.dev/modules/lfs"
"gitea.dev/modules/log" "gitea.dev/modules/log"
"gitea.dev/modules/process" "gitea.dev/modules/process"
@@ -102,6 +103,7 @@ func SyncPushMirror(ctx context.Context, mirrorID int64) bool {
log.Trace("SyncPushMirror [mirror: %d][repo: %-v]: Running Sync", m.ID, m.Repo) log.Trace("SyncPushMirror [mirror: %d][repo: %-v]: Running Sync", m.ID, m.Repo)
err = runPushSync(ctx, m) err = runPushSync(ctx, m)
if err != nil { if err != nil {
err = util.SanitizeErrorCredentialURLs(err)
log.Error("SyncPushMirror [mirror: %d][repo: %-v]: %v", m.ID, m.Repo, err) log.Error("SyncPushMirror [mirror: %d][repo: %-v]: %v", m.ID, m.Repo, err)
m.LastError = stripExitStatus.ReplaceAllLiteralString(err.Error(), "") m.LastError = stripExitStatus.ReplaceAllLiteralString(err.Error(), "")
} }
@@ -110,7 +112,6 @@ func SyncPushMirror(ctx context.Context, mirrorID int64) bool {
if err := repo_model.UpdatePushMirror(ctx, m); err != nil { if err := repo_model.UpdatePushMirror(ctx, m); err != nil {
log.Error("UpdatePushMirror [%d]: %v", m.ID, err) log.Error("UpdatePushMirror [%d]: %v", m.ID, err)
return false return false
} }
@@ -122,16 +123,17 @@ func SyncPushMirror(ctx context.Context, mirrorID int64) bool {
func runPushSync(ctx context.Context, m *repo_model.PushMirror) error { func runPushSync(ctx context.Context, m *repo_model.PushMirror) error {
timeout := time.Duration(setting.Git.Timeout.Mirror) * time.Second timeout := time.Duration(setting.Git.Timeout.Mirror) * time.Second
performPush := func(repo *repo_model.Repository, isWiki bool) error { performPush := func(storageRepo gitrepo.RepositoryFacade) error {
storageRepo := repo.CodeStorageRepo()
if isWiki {
storageRepo = repo.WikiStorageRepo()
}
mirrorLogName := fmt.Sprintf("%s%s[mirror=%d]", m.Repo.FullName(), util.Iif(isWiki, ".wiki", ""), m.ID)
remoteURL, err := git.ParseRemoteAddressURL(ctx, storageRepo, m.RemoteName) remoteURL, err := git.ParseRemoteAddressURL(ctx, storageRepo, m.RemoteName)
if err != nil { if err != nil {
log.Error("GetRemoteURL %s failed, error %v", mirrorLogName, err) return fmt.Errorf("ParseRemoteAddressURL failed: %w", err)
return errors.New("GitRemoteGetURL failed") }
// re-validate every sync, the allow/block lists may have changed since the mirror was added
switch remoteURL.URL.Scheme {
case "http", "https", "git":
if err := migrations.IsMigrateURLAllowed(remoteURL.String(), nil); err != nil {
return fmt.Errorf("remote address is not allowed: %w", err)
}
} }
if setting.LFS.StartServer { if setting.LFS.StartServer {
@@ -139,21 +141,20 @@ func runPushSync(ctx context.Context, m *repo_model.PushMirror) error {
gitRepo, err := git.OpenRepository(ctx, storageRepo) gitRepo, err := git.OpenRepository(ctx, storageRepo)
if err != nil { if err != nil {
log.Error("OpenRepository %s failed: %v", mirrorLogName, err) return fmt.Errorf("OpenRepository failed: %w", err)
return errors.New("OpenRepository failed")
} }
defer gitRepo.Close() defer gitRepo.Close()
lfsClient, err := lfs.NewClientFromEndpoint(remoteURL.String(), "", migrations.NewMigrationHTTPTransport()) lfsClient, err := lfs.NewClientFromEndpoint(remoteURL.String(), "", migrations.NewMigrationHTTPTransport())
if err != nil { if err != nil {
return err return fmt.Errorf("NewClientFromEndpoint failed: %w", err)
} }
if err := pushAllLFSObjects(ctx, gitRepo, lfsClient); err != nil { if err := pushAllLFSObjects(ctx, gitRepo, lfsClient); err != nil {
return util.SanitizeErrorCredentialURLs(err) return fmt.Errorf("pushAllLFSObjects failed: %w", err)
} }
} }
log.Trace("Pushing %s remote %s", mirrorLogName, m.ID, m.RemoteName) log.Trace("Pushing mirror %d repo %s to remote %s", m.ID, storageRepo.LogString(), m.RemoteName)
envs := proxy.EnvWithProxy(remoteURL.URL) envs := proxy.EnvWithProxy(remoteURL.URL)
if err := git.PushToExternal(ctx, storageRepo, git.PushOptions{ if err := git.PushToExternal(ctx, storageRepo, git.PushOptions{
@@ -163,26 +164,21 @@ func runPushSync(ctx context.Context, m *repo_model.PushMirror) error {
Timeout: timeout, Timeout: timeout,
Env: envs, Env: envs,
}); err != nil { }); err != nil {
log.Error("Error pushing %s remote %s: %v", mirrorLogName, m.RemoteName, err) return fmt.Errorf("PushToExternal failed: %w", err)
return util.SanitizeErrorCredentialURLs(err)
} }
return nil return nil
} }
err := performPush(m.Repo, false) err := performPush(m.Repo.CodeStorageRepo())
if err != nil { if err != nil {
return err return fmt.Errorf("performPush(code) failed: %w", err)
} }
if repo_service.HasWiki(ctx, m.Repo) { if repo_service.HasWiki(ctx, m.Repo) {
if _, err := git.ParseRemoteAddressURL(ctx, m.Repo.WikiStorageRepo(), m.RemoteName); err == nil { err := performPush(m.Repo.WikiStorageRepo())
err := performPush(m.Repo, true) if err != nil && !errors.Is(err, util.ErrNotExist) {
if err != nil { return fmt.Errorf("performPush(wiki) failed: %w", err)
return err
}
} else if !errors.Is(err, util.ErrNotExist) {
log.Error("GetRemote of wiki failed: %v", err)
} }
} }
+13 -37
View File
@@ -559,58 +559,34 @@ func checkIfPRContentChanged(ctx context.Context, pr *issues_model.PullRequest,
// PushToBaseRepo pushes commits from branches of head repository to // PushToBaseRepo pushes commits from branches of head repository to
// corresponding branches of base repository. // corresponding branches of base repository.
// FIXME: Only push branches that are actually updates? // FIXME: Only push branches that are actually updates?
func PushToBaseRepo(ctx context.Context, pr *issues_model.PullRequest) (err error) { func PushToBaseRepo(ctx context.Context, pr *issues_model.PullRequest) error {
return pushToBaseRepoHelper(ctx, pr, "")
}
func pushToBaseRepoHelper(ctx context.Context, pr *issues_model.PullRequest, prefixHeadBranch string) (err error) {
log.Trace("PushToBaseRepo[%d]: pushing commits to base repo '%s'", pr.BaseRepoID, pr.GetGitHeadRefName()) log.Trace("PushToBaseRepo[%d]: pushing commits to base repo '%s'", pr.BaseRepoID, pr.GetGitHeadRefName())
if err := pr.LoadHeadRepo(ctx); err != nil { if err := pr.LoadHeadRepo(ctx); err != nil {
log.Error("Unable to load head repository for PR[%d] Error: %v", pr.ID, err)
return err return err
} }
if err := pr.LoadBaseRepo(ctx); err != nil { if err := pr.LoadBaseRepo(ctx); err != nil {
log.Error("Unable to load base repository for PR[%d] Error: %v", pr.ID, err) return err
}
if err := pr.LoadIssue(ctx); err != nil {
return err
}
if err := pr.Issue.LoadPoster(ctx); err != nil {
return err return err
} }
if err = pr.LoadIssue(ctx); err != nil { baseRepoHeadRefName := pr.GetGitHeadRefName()
return fmt.Errorf("unable to load issue %d for pr %d: %w", pr.IssueID, pr.ID, err)
}
if err = pr.Issue.LoadPoster(ctx); err != nil {
return fmt.Errorf("unable to load poster %d for pr %d: %w", pr.Issue.PosterID, pr.ID, err)
}
gitRefName := pr.GetGitHeadRefName()
if err := git.PushManaged(ctx, pr.HeadRepo, pr.BaseRepo, git.PushOptions{ if err := git.PushManaged(ctx, pr.HeadRepo, pr.BaseRepo, git.PushOptions{
Branch: prefixHeadBranch + pr.HeadBranch + ":" + gitRefName, Branch: git.BranchPrefix + pr.HeadBranch + ":" + baseRepoHeadRefName,
Force: true, Force: true,
// Use InternalPushingEnvironment here because we know that pre-receive and post-receive do not run on a refs/pulls/... // Use InternalPushingEnvironment here because we know that pre-receive and post-receive do not run on a refs/pulls/...
Env: repo_module.InternalPushingEnvironment(pr.Issue.Poster, pr.BaseRepo), Env: repo_module.InternalPushingEnvironment(pr.Issue.Poster, pr.BaseRepo),
}); err != nil { }); err != nil {
if git.IsErrPushOutOfDate(err) { // Since we use internal force-push, there should be no git error.
// This should not happen as we're using force! // If any error happens, it must be an internal error (e.g.: broken git hooks) but not user error.
log.Error("Unable to push PR head for %s#%d (%-v:%s) due to ErrPushOfDate: %v", pr.BaseRepo.FullName(), pr.Index, pr.BaseRepo, gitRefName, err) return fmt.Errorf("unable to push from head branch %s:%s to base repo %s:%s, err: %w",
return err pr.HeadRepo.FullName(), pr.HeadBranch, pr.BaseRepo.FullName(), baseRepoHeadRefName, err)
} else if rejectErr, ok := err.(*git.ErrPushRejected); ok {
log.Info("Unable to push PR head for %s#%d (%-v:%s) due to rejection:\nStdout: %s\nStderr: %s\nError: %v", pr.BaseRepo.FullName(), pr.Index, pr.BaseRepo, gitRefName, rejectErr.StdOut, rejectErr.StdErr, rejectErr.Err)
return err
} else if git.IsErrMoreThanOne(err) {
if prefixHeadBranch != "" {
log.Info("Can't push with %s%s", prefixHeadBranch, pr.HeadBranch)
return err
}
log.Info("Retrying to push with %s%s", git.BranchPrefix, pr.HeadBranch)
err = pushToBaseRepoHelper(ctx, pr, git.BranchPrefix)
return err
}
log.Error("Unable to push PR head for %s#%d (%-v:%s) due to Error: %v", pr.BaseRepo.FullName(), pr.Index, pr.BaseRepo, gitRefName, err)
return fmt.Errorf("Push: %s:%s %s:%s %w", pr.HeadRepo.FullName(), pr.HeadBranch, pr.BaseRepo.FullName(), gitRefName, err)
} }
return nil return nil
} }
+1 -1
View File
@@ -145,7 +145,7 @@ func MigrateRepositoryGitData(ctx context.Context, u *user_model.User,
} }
if _, _, err := repo_module.SyncRepoBranchesWithRepo(ctx, repo, gitRepo, u.ID); err != nil { if _, _, err := repo_module.SyncRepoBranchesWithRepo(ctx, repo, gitRepo, u.ID); err != nil {
return repo, fmt.Errorf("SyncRepoBranchesWithRepo: %v", err) return repo, fmt.Errorf("SyncRepoBranchesWithRepo: %w", err)
} }
// if releases migration are not requested, we will sync all tags here // if releases migration are not requested, we will sync all tags here
+4 -4
View File
@@ -7,13 +7,13 @@ import (
"context" "context"
"errors" "errors"
"fmt" "fmt"
"strings"
"time" "time"
admin_model "gitea.dev/models/admin" admin_model "gitea.dev/models/admin"
"gitea.dev/models/db" "gitea.dev/models/db"
repo_model "gitea.dev/models/repo" repo_model "gitea.dev/models/repo"
user_model "gitea.dev/models/user" user_model "gitea.dev/models/user"
"gitea.dev/modules/git/gitcmd"
"gitea.dev/modules/graceful" "gitea.dev/modules/graceful"
"gitea.dev/modules/json" "gitea.dev/modules/json"
"gitea.dev/modules/log" "gitea.dev/modules/log"
@@ -145,10 +145,10 @@ func runMigrateTask(ctx context.Context, t *admin_model.Task) (err error) {
// remoteAddr may contain credentials, so we sanitize it // remoteAddr may contain credentials, so we sanitize it
err = util.SanitizeErrorCredentialURLs(err) err = util.SanitizeErrorCredentialURLs(err)
if strings.Contains(err.Error(), "Authentication failed") || if migrations.IsAuthenticationError(err) {
strings.Contains(err.Error(), "could not read Username") {
return fmt.Errorf("authentication failed: %w", err) return fmt.Errorf("authentication failed: %w", err)
} else if strings.Contains(err.Error(), "fatal:") { }
if _, fromGit := gitcmd.ErrorAsStderr(err); fromGit {
return fmt.Errorf("migration failed: %w", err) return fmt.Errorf("migration failed: %w", err)
} }
+2 -2
View File
@@ -19,11 +19,11 @@
</span> </span>
</div> </div>
<div class="inline field {{if .Err_Auth}}error{{end}}"> <div class="inline field">
<label for="auth_username">{{ctx.Locale.Tr "username"}}</label> <label for="auth_username">{{ctx.Locale.Tr "username"}}</label>
<input id="auth_username" name="auth_username" value="{{.auth_username}}" {{if not .auth_username}}data-need-clear="true"{{end}}> <input id="auth_username" name="auth_username" value="{{.auth_username}}" {{if not .auth_username}}data-need-clear="true"{{end}}>
</div> </div>
<div class="inline field {{if .Err_Auth}}error{{end}}"> <div class="inline field">
<label for="auth_password">{{ctx.Locale.Tr "password"}}</label> <label for="auth_password">{{ctx.Locale.Tr "password"}}</label>
<input id="auth_password" name="auth_password" type="password" value="{{.auth_password}}"> <input id="auth_password" name="auth_password" type="password" value="{{.auth_password}}">
</div> </div>
+4 -4
View File
@@ -19,19 +19,19 @@
</span> </span>
</div> </div>
<div class="inline required field {{if .Err_Auth}}error{{end}}"> <div class="inline required field">
<label for="aws_access_key_id">{{ctx.Locale.Tr "repo.migrate.codecommit.aws_access_key_id"}}</label> <label for="aws_access_key_id">{{ctx.Locale.Tr "repo.migrate.codecommit.aws_access_key_id"}}</label>
<input id="aws_access_key_id" name="aws_access_key_id" value="{{.aws_access_key_id}}" required> <input id="aws_access_key_id" name="aws_access_key_id" value="{{.aws_access_key_id}}" required>
</div> </div>
<div class="inline required field {{if .Err_Auth}}error{{end}}"> <div class="inline required field">
<label for="aws_secret_access_key">{{ctx.Locale.Tr "repo.migrate.codecommit.aws_secret_access_key"}}</label> <label for="aws_secret_access_key">{{ctx.Locale.Tr "repo.migrate.codecommit.aws_secret_access_key"}}</label>
<input id="aws_secret_access_key" name="aws_secret_access_key" type="password" value="{{.aws_secret_access_key}}" required> <input id="aws_secret_access_key" name="aws_secret_access_key" type="password" value="{{.aws_secret_access_key}}" required>
</div> </div>
<div class="inline required field {{if .Err_Auth}}error{{end}}"> <div class="inline required field">
<label for="auth_username">{{ctx.Locale.Tr "repo.migrate.codecommit.https_git_credentials_username"}}</label> <label for="auth_username">{{ctx.Locale.Tr "repo.migrate.codecommit.https_git_credentials_username"}}</label>
<input id="auth_username" name="auth_username" value="{{.auth_username}}" required> <input id="auth_username" name="auth_username" value="{{.auth_username}}" required>
</div> </div>
<div class="inline required field {{if .Err_Auth}}error{{end}}"> <div class="inline required field">
<label for="auth_password">{{ctx.Locale.Tr "repo.migrate.codecommit.https_git_credentials_password"}}</label> <label for="auth_password">{{ctx.Locale.Tr "repo.migrate.codecommit.https_git_credentials_password"}}</label>
<input id="auth_password" name="auth_password" type="password" value="{{.auth_password}}" required> <input id="auth_password" name="auth_password" type="password" value="{{.auth_password}}" required>
</div> </div>
+2 -2
View File
@@ -18,11 +18,11 @@
{{ctx.Locale.Tr "repo.migrate.clone_address_desc"}}{{if .ContextUser.CanImportLocal}} {{ctx.Locale.Tr "repo.migrate.clone_local_path"}}{{end}} {{ctx.Locale.Tr "repo.migrate.clone_address_desc"}}{{if .ContextUser.CanImportLocal}} {{ctx.Locale.Tr "repo.migrate.clone_local_path"}}{{end}}
</span> </span>
</div> </div>
<div class="inline field {{if .Err_Auth}}error{{end}}"> <div class="inline field">
<label for="auth_username">{{ctx.Locale.Tr "username"}}</label> <label for="auth_username">{{ctx.Locale.Tr "username"}}</label>
<input id="auth_username" name="auth_username" value="{{.auth_username}}" {{if not .auth_username}}data-need-clear="true"{{end}}> <input id="auth_username" name="auth_username" value="{{.auth_username}}" {{if not .auth_username}}data-need-clear="true"{{end}}>
</div> </div>
<div class="inline field {{if .Err_Auth}}error{{end}}"> <div class="inline field">
<label for="auth_password">{{ctx.Locale.Tr "password"}}</label> <label for="auth_password">{{ctx.Locale.Tr "password"}}</label>
<input id="auth_password" name="auth_password" type="password" value="{{.auth_password}}"> <input id="auth_password" name="auth_password" type="password" value="{{.auth_password}}">
</div> </div>
+2 -2
View File
@@ -19,11 +19,11 @@
</span> </span>
</div> </div>
<div class="inline field {{if .Err_Auth}}error{{end}}"> <div class="inline field">
<label for="auth_username">{{ctx.Locale.Tr "username"}}</label> <label for="auth_username">{{ctx.Locale.Tr "username"}}</label>
<input id="auth_username" name="auth_username" value="{{.auth_username}}" {{if not .auth_username}}data-need-clear="true"{{end}}> <input id="auth_username" name="auth_username" value="{{.auth_username}}" {{if not .auth_username}}data-need-clear="true"{{end}}>
</div> </div>
<div class="inline field {{if .Err_Auth}}error{{end}}"> <div class="inline field">
<label for="auth_password">{{ctx.Locale.Tr "password"}}</label> <label for="auth_password">{{ctx.Locale.Tr "password"}}</label>
<input id="auth_password" name="auth_password" type="password" value="{{.auth_password}}"> <input id="auth_password" name="auth_password" type="password" value="{{.auth_password}}">
</div> </div>
+1 -1
View File
@@ -18,7 +18,7 @@
</span> </span>
</div> </div>
<div class="inline field {{if .Err_Auth}}error{{end}}"> <div class="inline field">
<label for="auth_token">{{ctx.Locale.Tr "access_token"}}</label> <label for="auth_token">{{ctx.Locale.Tr "access_token"}}</label>
<input id="auth_token" name="auth_token" type="password" autocomplete="new-password" value="{{.auth_token}}" {{if not .auth_token}} data-need-clear="true" {{end}}> <input id="auth_token" name="auth_token" type="password" autocomplete="new-password" value="{{.auth_token}}" {{if not .auth_token}} data-need-clear="true" {{end}}>
<a target="_blank" href="https://docs.gitea.com/development/api-usage">{{svg "octicon-question"}}</a> <a target="_blank" href="https://docs.gitea.com/development/api-usage">{{svg "octicon-question"}}</a>
+1 -1
View File
@@ -18,7 +18,7 @@
</span> </span>
</div> </div>
<div class="inline field {{if .Err_Auth}}error{{end}}"> <div class="inline field">
<label for="auth_token">{{ctx.Locale.Tr "access_token"}}</label> <label for="auth_token">{{ctx.Locale.Tr "access_token"}}</label>
<input id="auth_token" name="auth_token" type="password" autocomplete="new-password" value="{{.auth_token}}" {{if not .auth_token}}data-need-clear="true"{{end}}> <input id="auth_token" name="auth_token" type="password" autocomplete="new-password" value="{{.auth_token}}" {{if not .auth_token}}data-need-clear="true"{{end}}>
<a target="_blank" href="https://docs.github.com/en/github/authenticating-to-github/creating-a-personal-access-token">{{svg "octicon-question"}}</a> <a target="_blank" href="https://docs.github.com/en/github/authenticating-to-github/creating-a-personal-access-token">{{svg "octicon-question"}}</a>
+1 -1
View File
@@ -18,7 +18,7 @@
</span> </span>
</div> </div>
<div class="inline field {{if .Err_Auth}}error{{end}}"> <div class="inline field">
<label for="auth_token">{{ctx.Locale.Tr "access_token"}}</label> <label for="auth_token">{{ctx.Locale.Tr "access_token"}}</label>
<input id="auth_token" name="auth_token" type="password" autocomplete="new-password" value="{{.auth_token}}" {{if not .auth_token}}data-need-clear="true"{{end}}> <input id="auth_token" name="auth_token" type="password" autocomplete="new-password" value="{{.auth_token}}" {{if not .auth_token}}data-need-clear="true"{{end}}>
<a target="_blank" href="https://docs.gitlab.com/ee/user/profile/personal_access_tokens.html">{{svg "octicon-question"}}</a> <a target="_blank" href="https://docs.gitlab.com/ee/user/profile/personal_access_tokens.html">{{svg "octicon-question"}}</a>
+1 -1
View File
@@ -18,7 +18,7 @@
</span> </span>
</div> </div>
<div class="inline field {{if .Err_Auth}}error{{end}}"> <div class="inline field">
<label for="auth_token">{{ctx.Locale.Tr "access_token"}}</label> <label for="auth_token">{{ctx.Locale.Tr "access_token"}}</label>
<input id="auth_token" name="auth_token" type="password" autocomplete="new-password" value="{{.auth_token}}" {{if not .auth_token}} data-need-clear="true" {{end}}> <input id="auth_token" name="auth_token" type="password" autocomplete="new-password" value="{{.auth_token}}" {{if not .auth_token}} data-need-clear="true" {{end}}>
<!-- <a target="_blank" href="https://docs.gitea.com/development/api-usage">{{svg "octicon-question"}}</a> --> <!-- <a target="_blank" href="https://docs.gitea.com/development/api-usage">{{svg "octicon-question"}}</a> -->
+2 -2
View File
@@ -19,11 +19,11 @@
</span> </span>
</div> </div>
<div class="inline field {{if .Err_Auth}}error{{end}}"> <div class="inline field">
<label for="auth_username">{{ctx.Locale.Tr "username"}}</label> <label for="auth_username">{{ctx.Locale.Tr "username"}}</label>
<input id="auth_username" name="auth_username" value="{{.auth_username}}" {{if not .auth_username}}data-need-clear="true"{{end}}> <input id="auth_username" name="auth_username" value="{{.auth_username}}" {{if not .auth_username}}data-need-clear="true"{{end}}>
</div> </div>
<div class="inline field {{if .Err_Auth}}error{{end}}"> <div class="inline field">
<label for="auth_password">{{ctx.Locale.Tr "password"}}</label> <label for="auth_password">{{ctx.Locale.Tr "password"}}</label>
<input id="auth_password" name="auth_password" type="password" value="{{.auth_password}}"> <input id="auth_password" name="auth_password" type="password" value="{{.auth_password}}">
</div> </div>
+3 -3
View File
@@ -154,16 +154,16 @@
<input id="mirror_address" name="mirror_address" value="{{$address.Address}}" required> <input id="mirror_address" name="mirror_address" value="{{$address.Address}}" required>
<p class="help">{{ctx.Locale.Tr "repo.mirror_address_desc"}}</p> <p class="help">{{ctx.Locale.Tr "repo.mirror_address_desc"}}</p>
</div> </div>
<details class="ui optional field" {{if or .Err_Auth $address.Username}}open{{end}}> <details class="ui optional field" {{if $address.Username}}open{{end}}>
<summary class="tw-p-1"> <summary class="tw-p-1">
{{ctx.Locale.Tr "repo.need_auth"}} {{ctx.Locale.Tr "repo.need_auth"}}
</summary> </summary>
<div class="tw-p-1"> <div class="tw-p-1">
<div class="inline field {{if .Err_Auth}}error{{end}}"> <div class="inline field">
<label for="mirror_username">{{ctx.Locale.Tr "username"}}</label> <label for="mirror_username">{{ctx.Locale.Tr "username"}}</label>
<input id="mirror_username" name="mirror_username" value="{{$address.Username}}" {{if not .mirror_username}}data-need-clear="true"{{end}}> <input id="mirror_username" name="mirror_username" value="{{$address.Username}}" {{if not .mirror_username}}data-need-clear="true"{{end}}>
</div> </div>
<div class="inline field {{if .Err_Auth}}error{{end}}"> <div class="inline field">
<label for="mirror_password">{{ctx.Locale.Tr "password"}}</label> <label for="mirror_password">{{ctx.Locale.Tr "password"}}</label>
<input id="mirror_password" name="mirror_password" type="password" placeholder="{{if $address.Password}}{{ctx.Locale.Tr "repo.mirror_password_placeholder"}}{{else}}{{ctx.Locale.Tr "repo.mirror_password_blank_placeholder"}}{{end}}" value="" {{if not .mirror_password}}data-need-clear="true"{{end}} autocomplete="off"> <input id="mirror_password" name="mirror_password" type="password" placeholder="{{if $address.Password}}{{ctx.Locale.Tr "repo.mirror_password_placeholder"}}{{else}}{{ctx.Locale.Tr "repo.mirror_password_blank_placeholder"}}{{end}}" value="" {{if not .mirror_password}}data-need-clear="true"{{end}} autocomplete="off">
</div> </div>
+5
View File
@@ -76,6 +76,11 @@ func testMirrorPush(t *testing.T, u *url.URL) {
assert.Equal(t, srcCommit.ID, mirrorCommit.ID) assert.Equal(t, srcCommit.ID, mirrorCommit.ID)
defer test.MockVariableValue(&setting.Migrations.AllowLocalNetworks, false)() // the remote is re-checked every sync, not just when added
assert.NoError(t, migrations.Init())
t.Cleanup(func() { _ = migrations.Init() })
assert.False(t, mirror_service.SyncPushMirror(t.Context(), mirrors[0].ID))
// Cleanup // Cleanup
assert.True(t, doRemovePushMirror(t, session, user.Name, srcRepo.Name, mirrors[0].ID)) assert.True(t, doRemovePushMirror(t, session, user.Name, srcRepo.Name, mirrors[0].ID))
mirrors, _, err = repo_model.GetPushMirrorsByRepoID(t.Context(), srcRepo.ID, db.ListOptions{}) mirrors, _, err = repo_model.GetPushMirrorsByRepoID(t.Context(), srcRepo.ID, db.ListOptions{})