From e940dd27b1aa2cb3db23035824fdc01c462c7d86 Mon Sep 17 00:00:00 2001 From: karthikbhandary2 Date: Thu, 26 Feb 2026 13:59:44 +0530 Subject: [PATCH] Verified and tested the DeleteRepoOrgs --- modules/structs/repo.go | 19 ++++-- routers/api/v1/api.go | 2 +- routers/api/v1/org/org.go | 4 +- routers/api/v1/swagger/org.go | 8 +++ templates/swagger/v1_json.tmpl | 88 ++++++++++++++++++++++++++++ tests/integration/api_org_test.go | 97 +++++++++++++++++++++++++++++++ 6 files changed, 211 insertions(+), 7 deletions(-) diff --git a/modules/structs/repo.go b/modules/structs/repo.go index a32871e0eb..30697feadd 100644 --- a/modules/structs/repo.go +++ b/modules/structs/repo.go @@ -441,14 +441,23 @@ type UpdateRepoAvatarOption struct { Image string `json:"image" binding:"Required"` } +// DeleteOrgReposResponse represents the response for deleting organization repositories +// swagger:model type DeleteOrgReposResponse struct { - SuccessCount int `json:"success_count"` - FailureCount int `json:"failure_count"` - Deleted []string `json:"deleted"` - Failed []DeleteRepoFailure `json:"failed"` + // 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 type DeleteRepoFailure struct { + // Repository name RepoName string `json:"repo_name"` - Reason string `json:"reason"` + // Reason for deletion failure + Reason string `json:"reason"` } diff --git a/routers/api/v1/api.go b/routers/api/v1/api.go index 9e80ee7728..cb88a0a520 100644 --- a/routers/api/v1/api.go +++ b/routers/api/v1/api.go @@ -1622,7 +1622,7 @@ func Routes() *web.Router { m.Post("/rename", reqToken(), reqOrgOwnership(), bind(api.RenameOrgOption{}), org.Rename) m.Combo("/repos").Get(user.ListOrgRepos). Post(reqToken(), bind(api.CreateRepoOption{}), repo.CreateOrgRepo). - Delete(reqToken(), reqOrgOwnership(), repo.DeleteOrgRepo) + Delete(reqToken(), reqOrgOwnership(), org.DeleteOrgRepos) m.Group("/members", func() { m.Get("", reqToken(), org.ListMembers) m.Combo("/{username}").Get(reqToken(), org.IsMember). diff --git a/routers/api/v1/org/org.go b/routers/api/v1/org/org.go index 20fb7c3e9b..4147b2e0f2 100644 --- a/routers/api/v1/org/org.go +++ b/routers/api/v1/org/org.go @@ -11,6 +11,7 @@ import ( "code.gitea.io/gitea/models/db" "code.gitea.io/gitea/models/organization" "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/optional" repo_module "code.gitea.io/gitea/modules/repository" @@ -22,6 +23,7 @@ import ( "code.gitea.io/gitea/services/convert" feed_service "code.gitea.io/gitea/services/feed" "code.gitea.io/gitea/services/org" + repo_service "code.gitea.io/gitea/services/repository" user_service "code.gitea.io/gitea/services/user" ) @@ -509,7 +511,7 @@ func DeleteOrgRepos(ctx *context.APIContext) { // required: true // responses: // "200": - // "$ref": "#/responses/DeleteOrgReposResponse" + // "$ref": "#/responses/DeleteOrgReposList" // "403": // "$ref": "#/responses/forbidden" org := ctx.Org.Organization diff --git a/routers/api/v1/swagger/org.go b/routers/api/v1/swagger/org.go index 0105446b00..f6cdbcab69 100644 --- a/routers/api/v1/swagger/org.go +++ b/routers/api/v1/swagger/org.go @@ -41,3 +41,11 @@ 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 a1ecc7fb4f..c87e4e3908 100644 --- a/templates/swagger/v1_json.tmpl +++ b/templates/swagger/v1_json.tmpl @@ -3638,6 +3638,33 @@ "$ref": "#/responses/notFound" } } + }, + "delete": { + "produces": [ + "application/json" + ], + "tags": [ + "organization" + ], + "summary": "Delete all repositories in an organization", + "operationId": "orgDeleteRepos", + "parameters": [ + { + "type": "string", + "description": "name of the organization", + "name": "org", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "$ref": "#/responses/DeleteOrgReposList" + }, + "403": { + "$ref": "#/responses/forbidden" + } + } } }, "/orgs/{org}/teams": { @@ -24180,6 +24207,58 @@ }, "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": "Reason for deletion failure", + "type": "string", + "x-go-name": "Reason" + }, + "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", @@ -29728,6 +29807,15 @@ } } }, + "DeleteOrgReposList": { + "description": "DeleteOrgReposList", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/DeleteOrgReposResponse" + } + } + }, "DeployKey": { "description": "DeployKey", "schema": { diff --git a/tests/integration/api_org_test.go b/tests/integration/api_org_test.go index 6b7826fbb8..f928658721 100644 --- a/tests/integration/api_org_test.go +++ b/tests/integration/api_org_test.go @@ -249,3 +249,100 @@ func TestAPIOrgGeneral(t *testing.T) { MakeRequest(t, req, http.StatusForbidden) }) } + +func TestAPIDeleteOrgRepos(t *testing.T) { + defer tests.PrepareTestEnv(t)() + + t.Run("Delete all repos successfully", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + + // Create test org with owner + session := loginUser(t, "user1") + token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteOrganization, auth_model.AccessTokenScopeWriteRepository) + + orgName := "test_delete_org" + req := NewRequestWithJSON(t, "POST", "/api/v1/orgs", &api.CreateOrgOption{ + UserName: orgName, + }).AddTokenAuth(token) + MakeRequest(t, req, http.StatusCreated) + + // Create 60 repos to test efficiency + for i := range 60 { + repoName := fmt.Sprintf("test_repo_%d", i) + req := NewRequestWithJSON(t, "POST", fmt.Sprintf("/api/v1/org/%s/repos", orgName), &api.CreateRepoOption{ + Name: repoName, + }).AddTokenAuth(token) + MakeRequest(t, req, http.StatusCreated) + } + + // Delete all repos + 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) + }) + + t.Run("Verify response structure", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + + session := loginUser(t, "user1") + token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteOrganization, auth_model.AccessTokenScopeWriteRepository) + + orgName := "test_response_org" + req := NewRequestWithJSON(t, "POST", "/api/v1/orgs", &api.CreateOrgOption{ + UserName: orgName, + }).AddTokenAuth(token) + MakeRequest(t, req, http.StatusCreated) + + // Create a few repos + for i := range 3 { + req = NewRequestWithJSON(t, "POST", fmt.Sprintf("/api/v1/org/%s/repos", orgName), &api.CreateRepoOption{ + Name: fmt.Sprintf("repo_%d", i), + }).AddTokenAuth(token) + MakeRequest(t, req, http.StatusCreated) + } + + // Delete all repos + 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) + }) + + t.Run("Fail without permissions", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + + // user2 is owner of org3 + ownerSession := loginUser(t, "user2") + ownerToken := getTokenForLoggedInUser(t, ownerSession, auth_model.AccessTokenScopeWriteOrganization, auth_model.AccessTokenScopeWriteRepository) + + // Create repo in org3 + req := NewRequestWithJSON(t, "POST", "/api/v1/org/org3/repos", &api.CreateRepoOption{ + Name: "test_perm_repo", + }).AddTokenAuth(ownerToken) + MakeRequest(t, req, http.StatusCreated) + + // user4 is not owner of org3 + nonOwnerSession := loginUser(t, "user4") + nonOwnerToken := getTokenForLoggedInUser(t, nonOwnerSession, auth_model.AccessTokenScopeWriteOrganization) + + // Try to delete repos without owner permission + req = NewRequest(t, "DELETE", "/api/v1/orgs/org3/repos").AddTokenAuth(nonOwnerToken) + MakeRequest(t, req, http.StatusForbidden) + }) +}