diff --git a/routers/api/v1/repo/mirror.go b/routers/api/v1/repo/mirror.go index ac2d8bba06a..bdca030b765 100644 --- a/routers/api/v1/repo/mirror.go +++ b/routers/api/v1/repo/mirror.go @@ -6,6 +6,7 @@ package repo import ( "errors" "net/http" + "strings" "time" "code.gitea.io/gitea/models/db" @@ -101,6 +102,8 @@ func PushMirrorSync(ctx *context.APIContext) { // "$ref": "#/responses/forbidden" // "404": // "$ref": "#/responses/notFound" + // "422": + // "$ref": "#/responses/validationError" if !setting.Mirror.Enabled { ctx.APIError(http.StatusBadRequest, "Mirror feature is disabled") @@ -112,14 +115,18 @@ func PushMirrorSync(ctx *context.APIContext) { ctx.APIError(http.StatusNotFound, err) return } + + failedPushMirrors := make([]string, 0) for _, mirror := range pushMirrors { ok := mirror_service.SyncPushMirror(ctx, mirror.ID) if !ok { - ctx.APIErrorInternal(errors.New("error occurred when syncing push mirror " + mirror.RemoteName)) - return + failedPushMirrors = append(failedPushMirrors, mirror.RemoteName) } } - + if len(failedPushMirrors) != 0 { + ctx.APIError(http.StatusUnprocessableEntity, "error occurred when syncing push mirrors: "+strings.Join(failedPushMirrors, ", ")) + return + } ctx.Status(http.StatusOK) } diff --git a/routers/api/v1/repo/mirror_test.go b/routers/api/v1/repo/mirror_test.go new file mode 100644 index 00000000000..6cbed49d899 --- /dev/null +++ b/routers/api/v1/repo/mirror_test.go @@ -0,0 +1,40 @@ +// Copyright 2026 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package repo + +import ( + "net/http" + "testing" + + "code.gitea.io/gitea/models/db" + repo_model "code.gitea.io/gitea/models/repo" + "code.gitea.io/gitea/models/unittest" + "code.gitea.io/gitea/modules/setting" + "code.gitea.io/gitea/modules/test" + "code.gitea.io/gitea/services/contexttest" + + "github.com/stretchr/testify/assert" +) + +// TestPushMirrorSync verifies the endpoint attempts every push mirror instead +// of aborting on the first failure, reporting all failed remotes with a 422. +// Each remote name is not a configured git remote, so SyncPushMirror fails fast +// without any network access. +func TestPushMirrorSync(t *testing.T) { + unittest.PrepareTestEnv(t) + defer test.MockVariableValue(&setting.Mirror.Enabled, true)() + + for _, remoteName := range []string{"broken_remote_1", "broken_remote_2"} { + assert.NoError(t, db.Insert(t.Context(), &repo_model.PushMirror{RepoID: 1, RemoteName: remoteName})) + } + + ctx, resp := contexttest.MockAPIContext(t, "user2/repo1") + contexttest.LoadRepo(t, ctx, 1) + + PushMirrorSync(ctx) + + assert.Equal(t, http.StatusUnprocessableEntity, ctx.Resp.WrittenStatus()) + assert.Contains(t, resp.Body.String(), "broken_remote_1") + assert.Contains(t, resp.Body.String(), "broken_remote_2") +} diff --git a/templates/swagger/v1_json.tmpl b/templates/swagger/v1_json.tmpl index 312eb25fd8d..d95699a48d6 100644 --- a/templates/swagger/v1_json.tmpl +++ b/templates/swagger/v1_json.tmpl @@ -15646,6 +15646,9 @@ }, "404": { "$ref": "#/responses/notFound" + }, + "422": { + "$ref": "#/responses/validationError" } } } diff --git a/templates/swagger/v1_openapi3_json.tmpl b/templates/swagger/v1_openapi3_json.tmpl index 04b8bd2d622..642154c5538 100644 --- a/templates/swagger/v1_openapi3_json.tmpl +++ b/templates/swagger/v1_openapi3_json.tmpl @@ -27446,6 +27446,9 @@ }, "404": { "$ref": "#/components/responses/notFound" + }, + "422": { + "$ref": "#/components/responses/validationError" } }, "summary": "Sync all push mirrored repository",