diff --git a/modules/structs/repo.go b/modules/structs/repo.go index 7f09acf2dc..a08cf36037 100644 --- a/modules/structs/repo.go +++ b/modules/structs/repo.go @@ -440,25 +440,3 @@ type UpdateRepoAvatarOption struct { // image must be base64 encoded Image string `json:"image" binding:"Required"` } - -// DeleteOrgReposResponse represents the response for deleting organization repositories -// swagger:model -type DeleteOrgReposResponse struct { - // Number of repositories successfully deleted - SuccessCount int `json:"success_count"` - // Number of repositories that failed to delete - FailureCount int `json:"failure_count"` - // List of repository names that were deleted - Deleted []string `json:"deleted"` - // Details about repositories that failed to delete - Failed []DeleteRepoFailure `json:"failed"` -} - -// DeleteRepoFailure represents a repository that failed to delete -// swagger:model -type DeleteRepoFailure struct { - // Repository name - RepoName string `json:"repo_name"` - // Message to be displayed - Message string `json:"reason"` -} diff --git a/routers/api/v1/org/org.go b/routers/api/v1/org/org.go index d225e6ba3d..e498c2c0de 100644 --- a/routers/api/v1/org/org.go +++ b/routers/api/v1/org/org.go @@ -13,6 +13,7 @@ import ( "code.gitea.io/gitea/models/perm" repo_model "code.gitea.io/gitea/models/repo" user_model "code.gitea.io/gitea/models/user" + "code.gitea.io/gitea/modules/graceful" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/optional" api "code.gitea.io/gitea/modules/structs" @@ -510,8 +511,8 @@ func DeleteOrgRepos(ctx *context.APIContext) { // type: string // required: true // responses: - // "200": - // "$ref": "#/responses/DeleteOrgReposList" + // "202": + // description: Deletion started // "403": // "$ref": "#/responses/forbidden" // "404": @@ -522,22 +523,29 @@ func DeleteOrgRepos(ctx *context.APIContext) { ctx.APIErrorInternal(err) return } - response := &api.DeleteOrgReposResponse{ - Deleted: []string{}, - Failed: []api.DeleteRepoFailure{}, - } - for _, repo := range repos { - if err := repo_service.DeleteRepository(ctx, ctx.Doer, repo, true); err != nil { - log.Error("Error deleting repo %s: %v", repo.Name, err) - response.Failed = append(response.Failed, api.DeleteRepoFailure{ - RepoName: repo.Name, - Message: "Failed to delete repository", - }) - } else { - response.Deleted = append(response.Deleted, repo.Name) + + doer := ctx.Doer + + // Start deletion in background with detached context + go func() { + defer func() { + if r := recover(); r != nil { + log.Error("Panic during org repo deletion: %v", r) + } + }() + + // Use HammerContext so deletion continues even if client disconnects + bgCtx := graceful.GetManager().HammerContext() + + for _, repo := range repos { + if err := repo_service.DeleteRepository(bgCtx, doer, repo, true); err != nil { + log.Error("Failed to delete repository %s (ID: %d) in org %s: %v", repo.Name, repo.ID, org.Name, err) + } else { + log.Info("Successfully deleted repository %s (ID: %d) in org %s", repo.Name, repo.ID, org.Name) + } } - } - response.SuccessCount = len(response.Deleted) - response.FailureCount = len(response.Failed) - ctx.JSON(http.StatusOK, response) + log.Info("Completed deletion of %d repositories in org %s", len(repos), org.Name) + }() + + ctx.Status(http.StatusAccepted) } diff --git a/routers/api/v1/swagger/org.go b/routers/api/v1/swagger/org.go index ef5b53d813..0105446b00 100644 --- a/routers/api/v1/swagger/org.go +++ b/routers/api/v1/swagger/org.go @@ -41,11 +41,3 @@ type swaggerResponseOrganizationPermissions struct { // in:body Body api.OrganizationPermissions `json:"body"` } - -// DeleteOrgReposList -// swagger:response DeleteOrgReposList -type swaggerDeleteOrgReposList struct { - // List of successfully deleted repositories and failures - //in:body - Body api.DeleteOrgReposResponse `json:"body"` -} diff --git a/templates/swagger/v1_json.tmpl b/templates/swagger/v1_json.tmpl index 6e3312be22..5216f3270e 100644 --- a/templates/swagger/v1_json.tmpl +++ b/templates/swagger/v1_json.tmpl @@ -3658,8 +3658,8 @@ } ], "responses": { - "200": { - "$ref": "#/responses/DeleteOrgReposList" + "202": { + "description": "Deletion started" }, "403": { "$ref": "#/responses/forbidden" @@ -24210,58 +24210,6 @@ }, "x-go-package": "code.gitea.io/gitea/modules/structs" }, - "DeleteOrgReposResponse": { - "description": "DeleteOrgReposResponse represents the response for deleting organization repositories", - "type": "object", - "properties": { - "deleted": { - "description": "List of repository names that were deleted", - "type": "array", - "items": { - "type": "string" - }, - "x-go-name": "Deleted" - }, - "failed": { - "description": "Details about repositories that failed to delete", - "type": "array", - "items": { - "$ref": "#/definitions/DeleteRepoFailure" - }, - "x-go-name": "Failed" - }, - "failure_count": { - "description": "Number of repositories that failed to delete", - "type": "integer", - "format": "int64", - "x-go-name": "FailureCount" - }, - "success_count": { - "description": "Number of repositories successfully deleted", - "type": "integer", - "format": "int64", - "x-go-name": "SuccessCount" - } - }, - "x-go-package": "code.gitea.io/gitea/modules/structs" - }, - "DeleteRepoFailure": { - "description": "DeleteRepoFailure represents a repository that failed to delete", - "type": "object", - "properties": { - "reason": { - "description": "Message to be displayed", - "type": "string", - "x-go-name": "Message" - }, - "repo_name": { - "description": "Repository name", - "type": "string", - "x-go-name": "RepoName" - } - }, - "x-go-package": "code.gitea.io/gitea/modules/structs" - }, "DeployKey": { "description": "DeployKey a deploy key", "type": "object", @@ -29810,12 +29758,6 @@ } } }, - "DeleteOrgReposList": { - "description": "DeleteOrgReposList", - "schema": { - "$ref": "#/definitions/DeleteOrgReposResponse" - } - }, "DeployKey": { "description": "DeployKey", "schema": { diff --git a/tests/integration/api_org_test.go b/tests/integration/api_org_test.go index f928658721..7678f7e0d0 100644 --- a/tests/integration/api_org_test.go +++ b/tests/integration/api_org_test.go @@ -275,17 +275,9 @@ func TestAPIDeleteOrgRepos(t *testing.T) { MakeRequest(t, req, http.StatusCreated) } - // Delete all repos + // Delete all repos - should return 202 Accepted req = NewRequest(t, "DELETE", fmt.Sprintf("/api/v1/orgs/%s/repos", orgName)).AddTokenAuth(token) - resp := MakeRequest(t, req, http.StatusOK) - - var result api.DeleteOrgReposResponse - DecodeJSON(t, resp, &result) - - assert.Equal(t, 60, result.SuccessCount) - assert.Equal(t, 0, result.FailureCount) - assert.Len(t, result.Deleted, 60) - assert.Empty(t, result.Failed) + MakeRequest(t, req, http.StatusAccepted) }) t.Run("Verify response structure", func(t *testing.T) { @@ -308,20 +300,9 @@ func TestAPIDeleteOrgRepos(t *testing.T) { MakeRequest(t, req, http.StatusCreated) } - // Delete all repos + // Delete all repos - should return 202 Accepted req = NewRequest(t, "DELETE", fmt.Sprintf("/api/v1/orgs/%s/repos", orgName)).AddTokenAuth(token) - resp := MakeRequest(t, req, http.StatusOK) - - var result api.DeleteOrgReposResponse - DecodeJSON(t, resp, &result) - - // Verify response structure - assert.Equal(t, 3, result.SuccessCount) - assert.Equal(t, 0, result.FailureCount) - assert.Len(t, result.Deleted, 3) - assert.Empty(t, result.Failed) - assert.NotNil(t, result.Deleted) - assert.NotNil(t, result.Failed) + MakeRequest(t, req, http.StatusAccepted) }) t.Run("Fail without permissions", func(t *testing.T) {