code.gitea.io/gitea@v1.22.3/services/repository/delete.go (about)

     1  // Copyright 2023 The Gitea Authors. All rights reserved.
     2  // SPDX-License-Identifier: MIT
     3  
     4  package repository
     5  
     6  import (
     7  	"context"
     8  	"fmt"
     9  
    10  	"code.gitea.io/gitea/models"
    11  	actions_model "code.gitea.io/gitea/models/actions"
    12  	activities_model "code.gitea.io/gitea/models/activities"
    13  	admin_model "code.gitea.io/gitea/models/admin"
    14  	asymkey_model "code.gitea.io/gitea/models/asymkey"
    15  	"code.gitea.io/gitea/models/db"
    16  	git_model "code.gitea.io/gitea/models/git"
    17  	issues_model "code.gitea.io/gitea/models/issues"
    18  	"code.gitea.io/gitea/models/organization"
    19  	access_model "code.gitea.io/gitea/models/perm/access"
    20  	project_model "code.gitea.io/gitea/models/project"
    21  	repo_model "code.gitea.io/gitea/models/repo"
    22  	secret_model "code.gitea.io/gitea/models/secret"
    23  	system_model "code.gitea.io/gitea/models/system"
    24  	user_model "code.gitea.io/gitea/models/user"
    25  	"code.gitea.io/gitea/models/webhook"
    26  	actions_module "code.gitea.io/gitea/modules/actions"
    27  	"code.gitea.io/gitea/modules/lfs"
    28  	"code.gitea.io/gitea/modules/log"
    29  	"code.gitea.io/gitea/modules/storage"
    30  	asymkey_service "code.gitea.io/gitea/services/asymkey"
    31  
    32  	"xorm.io/builder"
    33  )
    34  
    35  // DeleteRepository deletes a repository for a user or organization.
    36  // make sure if you call this func to close open sessions (sqlite will otherwise get a deadlock)
    37  func DeleteRepositoryDirectly(ctx context.Context, doer *user_model.User, repoID int64, ignoreOrgTeams ...bool) error {
    38  	ctx, committer, err := db.TxContext(ctx)
    39  	if err != nil {
    40  		return err
    41  	}
    42  	defer committer.Close()
    43  	sess := db.GetEngine(ctx)
    44  
    45  	repo := &repo_model.Repository{}
    46  	has, err := sess.ID(repoID).Get(repo)
    47  	if err != nil {
    48  		return err
    49  	} else if !has {
    50  		return repo_model.ErrRepoNotExist{
    51  			ID:        repoID,
    52  			OwnerName: "",
    53  			Name:      "",
    54  		}
    55  	}
    56  
    57  	// Query the action tasks of this repo, they will be needed after they have been deleted to remove the logs
    58  	tasks, err := db.Find[actions_model.ActionTask](ctx, actions_model.FindTaskOptions{RepoID: repoID})
    59  	if err != nil {
    60  		return fmt.Errorf("find actions tasks of repo %v: %w", repoID, err)
    61  	}
    62  
    63  	// Query the artifacts of this repo, they will be needed after they have been deleted to remove artifacts files in ObjectStorage
    64  	artifacts, err := db.Find[actions_model.ActionArtifact](ctx, actions_model.FindArtifactsOptions{RepoID: repoID})
    65  	if err != nil {
    66  		return fmt.Errorf("list actions artifacts of repo %v: %w", repoID, err)
    67  	}
    68  
    69  	// In case owner is a organization, we have to change repo specific teams
    70  	// if ignoreOrgTeams is not true
    71  	var org *user_model.User
    72  	if len(ignoreOrgTeams) == 0 || !ignoreOrgTeams[0] {
    73  		if org, err = user_model.GetUserByID(ctx, repo.OwnerID); err != nil {
    74  			return err
    75  		}
    76  	}
    77  
    78  	// Delete Deploy Keys
    79  	deployKeys, err := db.Find[asymkey_model.DeployKey](ctx, asymkey_model.ListDeployKeysOptions{RepoID: repoID})
    80  	if err != nil {
    81  		return fmt.Errorf("listDeployKeys: %w", err)
    82  	}
    83  	needRewriteKeysFile := len(deployKeys) > 0
    84  	for _, dKey := range deployKeys {
    85  		if err := models.DeleteDeployKey(ctx, doer, dKey.ID); err != nil {
    86  			return fmt.Errorf("deleteDeployKeys: %w", err)
    87  		}
    88  	}
    89  
    90  	if cnt, err := sess.ID(repoID).Delete(&repo_model.Repository{}); err != nil {
    91  		return err
    92  	} else if cnt != 1 {
    93  		return repo_model.ErrRepoNotExist{
    94  			ID:        repoID,
    95  			OwnerName: "",
    96  			Name:      "",
    97  		}
    98  	}
    99  
   100  	if org != nil && org.IsOrganization() {
   101  		teams, err := organization.FindOrgTeams(ctx, org.ID)
   102  		if err != nil {
   103  			return err
   104  		}
   105  		for _, t := range teams {
   106  			if !organization.HasTeamRepo(ctx, t.OrgID, t.ID, repoID) {
   107  				continue
   108  			} else if err = removeRepositoryFromTeam(ctx, t, repo, false); err != nil {
   109  				return err
   110  			}
   111  		}
   112  	}
   113  
   114  	attachments := make([]*repo_model.Attachment, 0, 20)
   115  	if err = sess.Join("INNER", "`release`", "`release`.id = `attachment`.release_id").
   116  		Where("`release`.repo_id = ?", repoID).
   117  		Find(&attachments); err != nil {
   118  		return err
   119  	}
   120  	releaseAttachments := make([]string, 0, len(attachments))
   121  	for i := 0; i < len(attachments); i++ {
   122  		releaseAttachments = append(releaseAttachments, attachments[i].RelativePath())
   123  	}
   124  
   125  	if _, err := db.Exec(ctx, "UPDATE `user` SET num_stars=num_stars-1 WHERE id IN (SELECT `uid` FROM `star` WHERE repo_id = ?)", repo.ID); err != nil {
   126  		return err
   127  	}
   128  
   129  	if _, err := db.GetEngine(ctx).In("hook_id", builder.Select("id").From("webhook").Where(builder.Eq{"webhook.repo_id": repo.ID})).
   130  		Delete(&webhook.HookTask{}); err != nil {
   131  		return err
   132  	}
   133  
   134  	if err := db.DeleteBeans(ctx,
   135  		&access_model.Access{RepoID: repo.ID},
   136  		&activities_model.Action{RepoID: repo.ID},
   137  		&repo_model.Collaboration{RepoID: repoID},
   138  		&issues_model.Comment{RefRepoID: repoID},
   139  		&git_model.CommitStatus{RepoID: repoID},
   140  		&git_model.Branch{RepoID: repoID},
   141  		&git_model.LFSLock{RepoID: repoID},
   142  		&repo_model.LanguageStat{RepoID: repoID},
   143  		&issues_model.Milestone{RepoID: repoID},
   144  		&repo_model.Mirror{RepoID: repoID},
   145  		&activities_model.Notification{RepoID: repoID},
   146  		&git_model.ProtectedBranch{RepoID: repoID},
   147  		&git_model.ProtectedTag{RepoID: repoID},
   148  		&repo_model.PushMirror{RepoID: repoID},
   149  		&repo_model.Release{RepoID: repoID},
   150  		&repo_model.RepoIndexerStatus{RepoID: repoID},
   151  		&repo_model.Redirect{RedirectRepoID: repoID},
   152  		&repo_model.RepoUnit{RepoID: repoID},
   153  		&repo_model.Star{RepoID: repoID},
   154  		&admin_model.Task{RepoID: repoID},
   155  		&repo_model.Watch{RepoID: repoID},
   156  		&webhook.Webhook{RepoID: repoID},
   157  		&secret_model.Secret{RepoID: repoID},
   158  		&actions_model.ActionTaskStep{RepoID: repoID},
   159  		&actions_model.ActionTask{RepoID: repoID},
   160  		&actions_model.ActionRunJob{RepoID: repoID},
   161  		&actions_model.ActionRun{RepoID: repoID},
   162  		&actions_model.ActionRunner{RepoID: repoID},
   163  		&actions_model.ActionScheduleSpec{RepoID: repoID},
   164  		&actions_model.ActionSchedule{RepoID: repoID},
   165  		&actions_model.ActionArtifact{RepoID: repoID},
   166  		&actions_model.ActionRunnerToken{RepoID: repoID},
   167  	); err != nil {
   168  		return fmt.Errorf("deleteBeans: %w", err)
   169  	}
   170  
   171  	// Delete Labels and related objects
   172  	if err := issues_model.DeleteLabelsByRepoID(ctx, repoID); err != nil {
   173  		return err
   174  	}
   175  
   176  	// Delete Pulls and related objects
   177  	if err := issues_model.DeletePullsByBaseRepoID(ctx, repoID); err != nil {
   178  		return err
   179  	}
   180  
   181  	// Delete Issues and related objects
   182  	var attachmentPaths []string
   183  	if attachmentPaths, err = issues_model.DeleteIssuesByRepoID(ctx, repoID); err != nil {
   184  		return err
   185  	}
   186  
   187  	// Delete issue index
   188  	if err := db.DeleteResourceIndex(ctx, "issue_index", repoID); err != nil {
   189  		return err
   190  	}
   191  
   192  	if repo.IsFork {
   193  		if _, err := db.Exec(ctx, "UPDATE `repository` SET num_forks=num_forks-1 WHERE id=?", repo.ForkID); err != nil {
   194  			return fmt.Errorf("decrease fork count: %w", err)
   195  		}
   196  	}
   197  
   198  	if _, err := db.Exec(ctx, "UPDATE `user` SET num_repos=num_repos-1 WHERE id=?", repo.OwnerID); err != nil {
   199  		return err
   200  	}
   201  
   202  	if len(repo.Topics) > 0 {
   203  		if err := repo_model.RemoveTopicsFromRepo(ctx, repo.ID); err != nil {
   204  			return err
   205  		}
   206  	}
   207  
   208  	if err := project_model.DeleteProjectByRepoID(ctx, repoID); err != nil {
   209  		return fmt.Errorf("unable to delete projects for repo[%d]: %w", repoID, err)
   210  	}
   211  
   212  	// Remove LFS objects
   213  	var lfsObjects []*git_model.LFSMetaObject
   214  	if err = sess.Where("repository_id=?", repoID).Find(&lfsObjects); err != nil {
   215  		return err
   216  	}
   217  
   218  	lfsPaths := make([]string, 0, len(lfsObjects))
   219  	for _, v := range lfsObjects {
   220  		count, err := db.CountByBean(ctx, &git_model.LFSMetaObject{Pointer: lfs.Pointer{Oid: v.Oid}})
   221  		if err != nil {
   222  			return err
   223  		}
   224  		if count > 1 {
   225  			continue
   226  		}
   227  
   228  		lfsPaths = append(lfsPaths, v.RelativePath())
   229  	}
   230  
   231  	if _, err := db.DeleteByBean(ctx, &git_model.LFSMetaObject{RepositoryID: repoID}); err != nil {
   232  		return err
   233  	}
   234  
   235  	// Remove archives
   236  	var archives []*repo_model.RepoArchiver
   237  	if err = sess.Where("repo_id=?", repoID).Find(&archives); err != nil {
   238  		return err
   239  	}
   240  
   241  	archivePaths := make([]string, 0, len(archives))
   242  	for _, v := range archives {
   243  		archivePaths = append(archivePaths, v.RelativePath())
   244  	}
   245  
   246  	if _, err := db.DeleteByBean(ctx, &repo_model.RepoArchiver{RepoID: repoID}); err != nil {
   247  		return err
   248  	}
   249  
   250  	if repo.NumForks > 0 {
   251  		if _, err = sess.Exec("UPDATE `repository` SET fork_id=0,is_fork=? WHERE fork_id=?", false, repo.ID); err != nil {
   252  			log.Error("reset 'fork_id' and 'is_fork': %v", err)
   253  		}
   254  	}
   255  
   256  	// Get all attachments with both issue_id and release_id are zero
   257  	var newAttachments []*repo_model.Attachment
   258  	if err := sess.Where(builder.Eq{
   259  		"repo_id":    repo.ID,
   260  		"issue_id":   0,
   261  		"release_id": 0,
   262  	}).Find(&newAttachments); err != nil {
   263  		return err
   264  	}
   265  
   266  	newAttachmentPaths := make([]string, 0, len(newAttachments))
   267  	for _, attach := range newAttachments {
   268  		newAttachmentPaths = append(newAttachmentPaths, attach.RelativePath())
   269  	}
   270  
   271  	if _, err := sess.Where("repo_id=?", repo.ID).Delete(new(repo_model.Attachment)); err != nil {
   272  		return err
   273  	}
   274  
   275  	if err = committer.Commit(); err != nil {
   276  		return err
   277  	}
   278  
   279  	committer.Close()
   280  
   281  	if needRewriteKeysFile {
   282  		if err := asymkey_service.RewriteAllPublicKeys(ctx); err != nil {
   283  			log.Error("RewriteAllPublicKeys failed: %v", err)
   284  		}
   285  	}
   286  
   287  	// We should always delete the files after the database transaction succeed. If
   288  	// we delete the file but the database rollback, the repository will be broken.
   289  
   290  	// Remove repository files.
   291  	repoPath := repo.RepoPath()
   292  	system_model.RemoveAllWithNotice(ctx, "Delete repository files", repoPath)
   293  
   294  	// Remove wiki files
   295  	if repo.HasWiki() {
   296  		system_model.RemoveAllWithNotice(ctx, "Delete repository wiki", repo.WikiPath())
   297  	}
   298  
   299  	// Remove archives
   300  	for _, archive := range archivePaths {
   301  		system_model.RemoveStorageWithNotice(ctx, storage.RepoArchives, "Delete repo archive file", archive)
   302  	}
   303  
   304  	// Remove lfs objects
   305  	for _, lfsObj := range lfsPaths {
   306  		system_model.RemoveStorageWithNotice(ctx, storage.LFS, "Delete orphaned LFS file", lfsObj)
   307  	}
   308  
   309  	// Remove issue attachment files.
   310  	for _, attachment := range attachmentPaths {
   311  		system_model.RemoveStorageWithNotice(ctx, storage.Attachments, "Delete issue attachment", attachment)
   312  	}
   313  
   314  	// Remove release attachment files.
   315  	for _, releaseAttachment := range releaseAttachments {
   316  		system_model.RemoveStorageWithNotice(ctx, storage.Attachments, "Delete release attachment", releaseAttachment)
   317  	}
   318  
   319  	// Remove attachment with no issue_id and release_id.
   320  	for _, newAttachment := range newAttachmentPaths {
   321  		system_model.RemoveStorageWithNotice(ctx, storage.Attachments, "Delete issue attachment", newAttachment)
   322  	}
   323  
   324  	if len(repo.Avatar) > 0 {
   325  		if err := storage.RepoAvatars.Delete(repo.CustomAvatarRelativePath()); err != nil {
   326  			return fmt.Errorf("Failed to remove %s: %w", repo.Avatar, err)
   327  		}
   328  	}
   329  
   330  	// Finally, delete action logs after the actions have already been deleted to avoid new log files
   331  	for _, task := range tasks {
   332  		err := actions_module.RemoveLogs(ctx, task.LogInStorage, task.LogFilename)
   333  		if err != nil {
   334  			log.Error("remove log file %q: %v", task.LogFilename, err)
   335  			// go on
   336  		}
   337  	}
   338  
   339  	// delete actions artifacts in ObjectStorage after the repo have already been deleted
   340  	for _, art := range artifacts {
   341  		if err := storage.ActionsArtifacts.Delete(art.StoragePath); err != nil {
   342  			log.Error("remove artifact file %q: %v", art.StoragePath, err)
   343  			// go on
   344  		}
   345  	}
   346  
   347  	return nil
   348  }
   349  
   350  // removeRepositoryFromTeam removes a repository from a team and recalculates access
   351  // Note: Repository shall not be removed from team if it includes all repositories (unless the repository is deleted)
   352  func removeRepositoryFromTeam(ctx context.Context, t *organization.Team, repo *repo_model.Repository, recalculate bool) (err error) {
   353  	e := db.GetEngine(ctx)
   354  	if err = organization.RemoveTeamRepo(ctx, t.ID, repo.ID); err != nil {
   355  		return err
   356  	}
   357  
   358  	t.NumRepos--
   359  	if _, err = e.ID(t.ID).Cols("num_repos").Update(t); err != nil {
   360  		return err
   361  	}
   362  
   363  	// Don't need to recalculate when delete a repository from organization.
   364  	if recalculate {
   365  		if err = access_model.RecalculateTeamAccesses(ctx, repo, t.ID); err != nil {
   366  			return err
   367  		}
   368  	}
   369  
   370  	teamMembers, err := organization.GetTeamMembers(ctx, &organization.SearchMembersOptions{
   371  		TeamID: t.ID,
   372  	})
   373  	if err != nil {
   374  		return fmt.Errorf("GetTeamMembers: %w", err)
   375  	}
   376  	for _, member := range teamMembers {
   377  		has, err := access_model.HasAnyUnitAccess(ctx, member.ID, repo)
   378  		if err != nil {
   379  			return err
   380  		} else if has {
   381  			continue
   382  		}
   383  
   384  		if err = repo_model.WatchRepo(ctx, member, repo, false); err != nil {
   385  			return err
   386  		}
   387  
   388  		// Remove all IssueWatches a user has subscribed to in the repositories
   389  		if err := issues_model.RemoveIssueWatchersByRepoID(ctx, member.ID, repo.ID); err != nil {
   390  			return err
   391  		}
   392  	}
   393  
   394  	return nil
   395  }
   396  
   397  // HasRepository returns true if given repository belong to team.
   398  func HasRepository(ctx context.Context, t *organization.Team, repoID int64) bool {
   399  	return organization.HasTeamRepo(ctx, t.OrgID, t.ID, repoID)
   400  }
   401  
   402  // RemoveRepositoryFromTeam removes repository from team of organization.
   403  // If the team shall include all repositories the request is ignored.
   404  func RemoveRepositoryFromTeam(ctx context.Context, t *organization.Team, repoID int64) error {
   405  	if !HasRepository(ctx, t, repoID) {
   406  		return nil
   407  	}
   408  
   409  	if t.IncludesAllRepositories {
   410  		return nil
   411  	}
   412  
   413  	repo, err := repo_model.GetRepositoryByID(ctx, repoID)
   414  	if err != nil {
   415  		return err
   416  	}
   417  
   418  	ctx, committer, err := db.TxContext(ctx)
   419  	if err != nil {
   420  		return err
   421  	}
   422  	defer committer.Close()
   423  
   424  	if err = removeRepositoryFromTeam(ctx, t, repo, true); err != nil {
   425  		return err
   426  	}
   427  
   428  	return committer.Commit()
   429  }
   430  
   431  // DeleteOwnerRepositoriesDirectly calls DeleteRepositoryDirectly for all repos of the given owner
   432  func DeleteOwnerRepositoriesDirectly(ctx context.Context, owner *user_model.User) error {
   433  	for {
   434  		repos, _, err := repo_model.GetUserRepositories(ctx, &repo_model.SearchRepoOptions{
   435  			ListOptions: db.ListOptions{
   436  				PageSize: repo_model.RepositoryListDefaultPageSize,
   437  				Page:     1,
   438  			},
   439  			Private: true,
   440  			OwnerID: owner.ID,
   441  			Actor:   owner,
   442  		})
   443  		if err != nil {
   444  			return fmt.Errorf("GetUserRepositories: %w", err)
   445  		}
   446  		if len(repos) == 0 {
   447  			break
   448  		}
   449  		for _, repo := range repos {
   450  			if err := DeleteRepositoryDirectly(ctx, owner, repo.ID); err != nil {
   451  				return fmt.Errorf("unable to delete repository %s for %s[%d]. Error: %w", repo.Name, owner.Name, owner.ID, err)
   452  			}
   453  		}
   454  	}
   455  	return nil
   456  }