fix: delete repo-scoped rows of seven more tables when deleting a repository (#38534)

Fixes #38494

`DeleteRepositoryDirectly` left rows behind in seven registered tables
carrying a repo-scoped key: `action_variable`, `action_run_attempt`,
`action_tasks_version`, `renamed_branch`, `commit_status_summary`,
`commit_status_index` and `repo_transfer`. Repository IDs are `pk
autoincr` and never reissued, so the orphaned rows were unreachable, but
they accumulated forever (unbounded table growth, referential
inconsistency; not a security issue, see the issue discussion).

This adds all seven to the `deleteBeans` cascade, each placed next to
its sibling bean
(`CommitStatus`/`CommitStatusIndex`/`CommitStatusSummary`,
`Branch`/`RenamedBranch`, `Secret`/`ActionVariable`,
`ActionRun`/`ActionRunAttempt`). `repo_transfer` goes through the same
cascade rather than `DeleteRepositoryTransfer` so teardown stays one
mechanism; the existing reaper remains for the transfer flows.

The test inserts one row per previously-orphaned table (the fixture
files ask test cases to prepare their own data), deletes the repository,
and asserts each table is purged. Without the fix it fails on all seven
tables.

---------

Signed-off-by: wxiaoguang <wxiaoguang@gmail.com>
Signed-off-by: Luc <luuuc@users.noreply.github.com>
Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
This commit is contained in:
Luc
2026-07-24 17:40:39 +00:00
committed by GitHub
co-authored by wxiaoguang
parent 66794e0549
commit 9ad80e1eba
2 changed files with 35 additions and 0 deletions
+7
View File
@@ -150,7 +150,10 @@ func DeleteRepositoryDirectly(ctx context.Context, repoID int64, ignoreOrgTeams
&repo_model.Collaboration{RepoID: repoID},
&issues_model.Comment{RefRepoID: repoID},
&git_model.CommitStatus{RepoID: repoID},
&git_model.CommitStatusIndex{RepoID: repoID},
&git_model.CommitStatusSummary{RepoID: repoID},
&git_model.Branch{RepoID: repoID},
&git_model.RenamedBranch{RepoID: repoID},
&git_model.LFSLock{RepoID: repoID},
&repo_model.LanguageStat{RepoID: repoID},
&repo_model.RepoLicense{RepoID: repoID},
@@ -163,22 +166,26 @@ func DeleteRepositoryDirectly(ctx context.Context, repoID int64, ignoreOrgTeams
&repo_model.Release{RepoID: repoID},
&repo_model.RepoIndexerStatus{RepoID: repoID},
&repo_model.Redirect{RedirectRepoID: repoID},
&repo_model.RepoTransfer{RepoID: repoID}, // this column doesn't have index, maybe it's fine since the table shouldn't be too large.
&repo_model.RepoUnit{RepoID: repoID},
&repo_model.Star{RepoID: repoID},
&admin_model.Task{RepoID: repoID},
&repo_model.Watch{RepoID: repoID},
&webhook.Webhook{RepoID: repoID},
&secret_model.Secret{RepoID: repoID},
&actions_model.ActionVariable{RepoID: repoID},
&actions_model.ActionTaskStep{RepoID: repoID},
&actions_model.ActionTask{RepoID: repoID},
&actions_model.ActionRunJob{RepoID: repoID},
&actions_model.ActionRun{RepoID: repoID},
&actions_model.ActionRunAttempt{RepoID: repoID},
&actions_model.ActionRunner{RepoID: repoID},
&actions_model.ActionScheduleSpec{RepoID: repoID},
&actions_model.ActionSchedule{RepoID: repoID},
&actions_model.ActionArtifact{RepoID: repoID},
&actions_model.ActionRunJobSummary{RepoID: repoID},
&actions_model.ActionRunnerToken{RepoID: repoID},
&actions_model.ActionTasksVersion{RepoID: repoID},
&actions_model.ActionScopedWorkflowSource{SourceRepoID: repoID},
&issues_model.IssuePin{RepoID: repoID},
); err != nil {
+28
View File
@@ -6,6 +6,9 @@ package repository_test
import (
"testing"
actions_model "gitea.dev/models/actions"
"gitea.dev/models/db"
git_model "gitea.dev/models/git"
"gitea.dev/models/organization"
repo_model "gitea.dev/models/repo"
"gitea.dev/models/unittest"
@@ -52,3 +55,28 @@ func TestDeleteOwnerRepositoriesDirectly(t *testing.T) {
assert.NoError(t, repo_service.DeleteOwnerRepositoriesDirectly(t.Context(), user))
}
func TestDeleteRepositoryDirectlyPurgesRepoScopedRows(t *testing.T) {
unittest.PrepareTestEnv(t)
// One row per table that repository deletion used to leave behind (#38494).
assert.NoError(t, db.Insert(t.Context(),
&actions_model.ActionVariable{RepoID: 1, Name: "to_purge", Data: "value"},
&actions_model.ActionRunAttempt{RepoID: 1, RunID: unittest.NonexistentID, Attempt: 1},
&actions_model.ActionTasksVersion{RepoID: 1, Version: 1},
&git_model.RenamedBranch{RepoID: 1, From: "old-name", To: "new-name"},
&git_model.CommitStatusSummary{RepoID: 1, SHA: "deadbeefdeadbeefdeadbeefdeadbeefdeadbeef", State: "success"},
&repo_model.RepoTransfer{RepoID: 1, DoerID: 2, RecipientID: 3},
))
unittest.AssertExistsAndLoadBean(t, &git_model.CommitStatusIndex{RepoID: 1})
assert.NoError(t, repo_service.DeleteRepositoryDirectly(t.Context(), 1))
unittest.AssertNotExistsBean(t, &actions_model.ActionVariable{RepoID: 1})
unittest.AssertNotExistsBean(t, &actions_model.ActionRunAttempt{RepoID: 1})
unittest.AssertNotExistsBean(t, &actions_model.ActionTasksVersion{RepoID: 1})
unittest.AssertNotExistsBean(t, &git_model.RenamedBranch{RepoID: 1})
unittest.AssertNotExistsBean(t, &git_model.CommitStatusSummary{RepoID: 1})
unittest.AssertNotExistsBean(t, &git_model.CommitStatusIndex{RepoID: 1})
unittest.AssertNotExistsBean(t, &repo_model.RepoTransfer{RepoID: 1})
}