mirror of
https://github.com/go-gitea/gitea.git
synced 2026-09-19 11:13:41 +09:00
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:
@@ -6,6 +6,9 @@ package migrations
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
|
||||
"gitea.dev/modules/git/gitcmd"
|
||||
|
||||
"github.com/google/go-github/v91/github"
|
||||
)
|
||||
@@ -24,3 +27,12 @@ func IsTwoFactorAuthError(err error) bool {
|
||||
_, ok := err.(*github.TwoFactorAuthError)
|
||||
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
|
||||
}
|
||||
|
||||
@@ -130,7 +130,8 @@ func MigrateRepository(ctx context.Context, doer *user_model.User, ownerName str
|
||||
if err1 := uploader.Rollback(); err1 != nil {
|
||||
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)
|
||||
}
|
||||
return nil, err
|
||||
|
||||
@@ -4,17 +4,44 @@
|
||||
package migrations
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"gitea.dev/models/unittest"
|
||||
user_model "gitea.dev/models/user"
|
||||
"gitea.dev/modules/git/gitcmd"
|
||||
"gitea.dev/modules/setting"
|
||||
"gitea.dev/modules/util"
|
||||
|
||||
"github.com/google/go-github/v91/github"
|
||||
"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) {
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
|
||||
|
||||
@@ -14,6 +14,7 @@ import (
|
||||
"gitea.dev/models/db"
|
||||
repo_model "gitea.dev/models/repo"
|
||||
"gitea.dev/modules/git"
|
||||
"gitea.dev/modules/git/gitrepo"
|
||||
"gitea.dev/modules/lfs"
|
||||
"gitea.dev/modules/log"
|
||||
"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)
|
||||
err = runPushSync(ctx, m)
|
||||
if err != nil {
|
||||
err = util.SanitizeErrorCredentialURLs(err)
|
||||
log.Error("SyncPushMirror [mirror: %d][repo: %-v]: %v", m.ID, m.Repo, err)
|
||||
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 {
|
||||
log.Error("UpdatePushMirror [%d]: %v", m.ID, err)
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -122,16 +123,17 @@ func SyncPushMirror(ctx context.Context, mirrorID int64) bool {
|
||||
func runPushSync(ctx context.Context, m *repo_model.PushMirror) error {
|
||||
timeout := time.Duration(setting.Git.Timeout.Mirror) * time.Second
|
||||
|
||||
performPush := func(repo *repo_model.Repository, isWiki bool) 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)
|
||||
performPush := func(storageRepo gitrepo.RepositoryFacade) error {
|
||||
remoteURL, err := git.ParseRemoteAddressURL(ctx, storageRepo, m.RemoteName)
|
||||
if err != nil {
|
||||
log.Error("GetRemoteURL %s failed, error %v", mirrorLogName, err)
|
||||
return errors.New("GitRemoteGetURL failed")
|
||||
return fmt.Errorf("ParseRemoteAddressURL failed: %w", err)
|
||||
}
|
||||
// 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 {
|
||||
@@ -139,21 +141,20 @@ func runPushSync(ctx context.Context, m *repo_model.PushMirror) error {
|
||||
|
||||
gitRepo, err := git.OpenRepository(ctx, storageRepo)
|
||||
if err != nil {
|
||||
log.Error("OpenRepository %s failed: %v", mirrorLogName, err)
|
||||
return errors.New("OpenRepository failed")
|
||||
return fmt.Errorf("OpenRepository failed: %w", err)
|
||||
}
|
||||
defer gitRepo.Close()
|
||||
|
||||
lfsClient, err := lfs.NewClientFromEndpoint(remoteURL.String(), "", migrations.NewMigrationHTTPTransport())
|
||||
if err != nil {
|
||||
return err
|
||||
return fmt.Errorf("NewClientFromEndpoint failed: %w", err)
|
||||
}
|
||||
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)
|
||||
if err := git.PushToExternal(ctx, storageRepo, git.PushOptions{
|
||||
@@ -163,26 +164,21 @@ func runPushSync(ctx context.Context, m *repo_model.PushMirror) error {
|
||||
Timeout: timeout,
|
||||
Env: envs,
|
||||
}); err != nil {
|
||||
log.Error("Error pushing %s remote %s: %v", mirrorLogName, m.RemoteName, err)
|
||||
return util.SanitizeErrorCredentialURLs(err)
|
||||
return fmt.Errorf("PushToExternal failed: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
err := performPush(m.Repo, false)
|
||||
err := performPush(m.Repo.CodeStorageRepo())
|
||||
if err != nil {
|
||||
return err
|
||||
return fmt.Errorf("performPush(code) failed: %w", err)
|
||||
}
|
||||
|
||||
if repo_service.HasWiki(ctx, m.Repo) {
|
||||
if _, err := git.ParseRemoteAddressURL(ctx, m.Repo.WikiStorageRepo(), m.RemoteName); err == nil {
|
||||
err := performPush(m.Repo, true)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
} else if !errors.Is(err, util.ErrNotExist) {
|
||||
log.Error("GetRemote of wiki failed: %v", err)
|
||||
err := performPush(m.Repo.WikiStorageRepo())
|
||||
if err != nil && !errors.Is(err, util.ErrNotExist) {
|
||||
return fmt.Errorf("performPush(wiki) failed: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+13
-37
@@ -559,58 +559,34 @@ func checkIfPRContentChanged(ctx context.Context, pr *issues_model.PullRequest,
|
||||
// PushToBaseRepo pushes commits from branches of head repository to
|
||||
// corresponding branches of base repository.
|
||||
// FIXME: Only push branches that are actually updates?
|
||||
func PushToBaseRepo(ctx context.Context, pr *issues_model.PullRequest) (err error) {
|
||||
return pushToBaseRepoHelper(ctx, pr, "")
|
||||
}
|
||||
|
||||
func pushToBaseRepoHelper(ctx context.Context, pr *issues_model.PullRequest, prefixHeadBranch string) (err error) {
|
||||
func PushToBaseRepo(ctx context.Context, pr *issues_model.PullRequest) error {
|
||||
log.Trace("PushToBaseRepo[%d]: pushing commits to base repo '%s'", pr.BaseRepoID, pr.GetGitHeadRefName())
|
||||
|
||||
if err := pr.LoadHeadRepo(ctx); err != nil {
|
||||
log.Error("Unable to load head repository for PR[%d] Error: %v", pr.ID, err)
|
||||
return err
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
if err = pr.LoadIssue(ctx); err != nil {
|
||||
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()
|
||||
|
||||
baseRepoHeadRefName := pr.GetGitHeadRefName()
|
||||
if err := git.PushManaged(ctx, pr.HeadRepo, pr.BaseRepo, git.PushOptions{
|
||||
Branch: prefixHeadBranch + pr.HeadBranch + ":" + gitRefName,
|
||||
Branch: git.BranchPrefix + pr.HeadBranch + ":" + baseRepoHeadRefName,
|
||||
Force: true,
|
||||
// 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),
|
||||
}); err != nil {
|
||||
if git.IsErrPushOutOfDate(err) {
|
||||
// This should not happen as we're using force!
|
||||
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 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)
|
||||
// Since we use internal force-push, there should be no git error.
|
||||
// If any error happens, it must be an internal error (e.g.: broken git hooks) but not user error.
|
||||
return fmt.Errorf("unable to push from head branch %s:%s to base repo %s:%s, err: %w",
|
||||
pr.HeadRepo.FullName(), pr.HeadBranch, pr.BaseRepo.FullName(), baseRepoHeadRefName, err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@@ -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 {
|
||||
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
|
||||
|
||||
@@ -7,13 +7,13 @@ import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
admin_model "gitea.dev/models/admin"
|
||||
"gitea.dev/models/db"
|
||||
repo_model "gitea.dev/models/repo"
|
||||
user_model "gitea.dev/models/user"
|
||||
"gitea.dev/modules/git/gitcmd"
|
||||
"gitea.dev/modules/graceful"
|
||||
"gitea.dev/modules/json"
|
||||
"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
|
||||
err = util.SanitizeErrorCredentialURLs(err)
|
||||
if strings.Contains(err.Error(), "Authentication failed") ||
|
||||
strings.Contains(err.Error(), "could not read Username") {
|
||||
if migrations.IsAuthenticationError(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)
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user