code.gitea.io/gitea@v1.21.7/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  
    31  	"xorm.io/builder"
    32  )
    33  
    34  // DeleteRepository deletes a repository for a user or organization.
    35  // make sure if you call this func to close open sessions (sqlite will otherwise get a deadlock)
    36  func DeleteRepositoryDirectly(ctx context.Context, doer *user_model.User, uid, repoID int64, ignoreOrgTeams ...bool) error {
    37  	ctx, committer, err := db.TxContext(ctx)
    38  	if err != nil {
    39  		return err
    40  	}
    41  	defer committer.Close()
    42  	sess := db.GetEngine(ctx)
    43  
    44  	// Query the action tasks of this repo, they will be needed after they have been deleted to remove the logs
    45  	tasks, err := actions_model.FindTasks(ctx, actions_model.FindTaskOptions{RepoID: repoID})
    46  	if err != nil {
    47  		return fmt.Errorf("find actions tasks of repo %v: %w", repoID, err)
    48  	}
    49  
    50  	// Query the artifacts of this repo, they will be needed after they have been deleted to remove artifacts files in ObjectStorage
    51  	artifacts, err := actions_model.ListArtifactsByRepoID(ctx, repoID)
    52  	if err != nil {
    53  		return fmt.Errorf("list actions artifacts of repo %v: %w", repoID, err)
    54  	}
    55  
    56  	// In case owner is a organization, we have to change repo specific teams
    57  	// if ignoreOrgTeams is not true
    58  	var org *user_model.User
    59  	if len(ignoreOrgTeams) == 0 || !ignoreOrgTeams[0] {
    60  		if org, err = user_model.GetUserByID(ctx, uid); err != nil {
    61  			return err
    62  		}
    63  	}
    64  
    65  	repo := &repo_model.Repository{OwnerID: uid}
    66  	has, err := sess.ID(repoID).Get(repo)
    67  	if err != nil {
    68  		return err
    69  	} else if !has {
    70  		return repo_model.ErrRepoNotExist{
    71  			ID:        repoID,
    72  			UID:       uid,
    73  			OwnerName: "",
    74  			Name:      "",
    75  		}
    76  	}
    77  
    78  	// Delete Deploy Keys
    79  	deployKeys, err := asymkey_model.ListDeployKeys(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  			UID:       uid,
    96  			OwnerName: "",
    97  			Name:      "",
    98  		}
    99  	}
   100  
   101  	if org != nil && org.IsOrganization() {
   102  		teams, err := organization.FindOrgTeams(ctx, org.ID)
   103  		if err != nil {
   104  			return err
   105  		}
   106  		for _, t := range teams {
   107  			if !organization.HasTeamRepo(ctx, t.OrgID, t.ID, repoID) {
   108  				continue
   109  			} else if err = removeRepositoryFromTeam(ctx, t, repo, false); err != nil {
   110  				return err
   111  			}
   112  		}
   113  	}
   114  
   115  	attachments := make([]*repo_model.Attachment, 0, 20)
   116  	if err = sess.Join("INNER", "`release`", "`release`.id = `attachment`.release_id").
   117  		Where("`release`.repo_id = ?", repoID).
   118  		Find(&attachments); err != nil {
   119  		return err
   120  	}
   121  	releaseAttachments := make([]string, 0, len(attachments))
   122  	for i := 0; i < len(attachments); i++ {
   123  		releaseAttachments = append(releaseAttachments, attachments[i].RelativePath())
   124  	}
   125  
   126  	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 {
   127  		return err
   128  	}
   129  
   130  	if _, err := db.GetEngine(ctx).In("hook_id", builder.Select("id").From("webhook").Where(builder.Eq{"webhook.repo_id": repo.ID})).
   131  		Delete(&webhook.HookTask{}); err != nil {
   132  		return err
   133  	}
   134  
   135  	if err := db.DeleteBeans(ctx,
   136  		&access_model.Access{RepoID: repo.ID},
   137  		&activities_model.Action{RepoID: repo.ID},
   138  		&repo_model.Collaboration{RepoID: repoID},
   139  		&issues_model.Comment{RefRepoID: repoID},
   140  		&git_model.CommitStatus{RepoID: repoID},
   141  		&git_model.Branch{RepoID: repoID},
   142  		&git_model.LFSLock{RepoID: repoID},
   143  		&repo_model.LanguageStat{RepoID: repoID},
   144  		&issues_model.Milestone{RepoID: repoID},
   145  		&repo_model.Mirror{RepoID: repoID},
   146  		&activities_model.Notification{RepoID: repoID},
   147  		&git_model.ProtectedBranch{RepoID: repoID},
   148  		&git_model.ProtectedTag{RepoID: repoID},
   149  		&repo_model.PushMirror{RepoID: repoID},
   150  		&repo_model.Release{RepoID: repoID},
   151  		&repo_model.RepoIndexerStatus{RepoID: repoID},
   152  		&repo_model.Redirect{RedirectRepoID: repoID},
   153  		&repo_model.RepoUnit{RepoID: repoID},
   154  		&repo_model.Star{RepoID: repoID},
   155  		&admin_model.Task{RepoID: repoID},
   156  		&repo_model.Watch{RepoID: repoID},
   157  		&webhook.Webhook{RepoID: repoID},
   158  		&secret_model.Secret{RepoID: repoID},
   159  		&actions_model.ActionTaskStep{RepoID: repoID},
   160  		&actions_model.ActionTask{RepoID: repoID},
   161  		&actions_model.ActionRunJob{RepoID: repoID},
   162  		&actions_model.ActionRun{RepoID: repoID},
   163  		&actions_model.ActionRunner{RepoID: repoID},
   164  		&actions_model.ActionScheduleSpec{RepoID: repoID},
   165  		&actions_model.ActionSchedule{RepoID: repoID},
   166  		&actions_model.ActionArtifact{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=?", uid); 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_model.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  	teamUsers, err := organization.GetTeamUsersByTeamID(ctx, t.ID)
   371  	if err != nil {
   372  		return fmt.Errorf("getTeamUsersByTeamID: %w", err)
   373  	}
   374  	for _, teamUser := range teamUsers {
   375  		has, err := access_model.HasAccess(ctx, teamUser.UID, repo)
   376  		if err != nil {
   377  			return err
   378  		} else if has {
   379  			continue
   380  		}
   381  
   382  		if err = repo_model.WatchRepo(ctx, teamUser.UID, repo.ID, false); err != nil {
   383  			return err
   384  		}
   385  
   386  		// Remove all IssueWatches a user has subscribed to in the repositories
   387  		if err := issues_model.RemoveIssueWatchersByRepoID(ctx, teamUser.UID, repo.ID); err != nil {
   388  			return err
   389  		}
   390  	}
   391  
   392  	return nil
   393  }
   394  
   395  // HasRepository returns true if given repository belong to team.
   396  func HasRepository(ctx context.Context, t *organization.Team, repoID int64) bool {
   397  	return organization.HasTeamRepo(ctx, t.OrgID, t.ID, repoID)
   398  }
   399  
   400  // RemoveRepositoryFromTeam removes repository from team of organization.
   401  // If the team shall include all repositories the request is ignored.
   402  func RemoveRepositoryFromTeam(ctx context.Context, t *organization.Team, repoID int64) error {
   403  	if !HasRepository(ctx, t, repoID) {
   404  		return nil
   405  	}
   406  
   407  	if t.IncludesAllRepositories {
   408  		return nil
   409  	}
   410  
   411  	repo, err := repo_model.GetRepositoryByID(ctx, repoID)
   412  	if err != nil {
   413  		return err
   414  	}
   415  
   416  	ctx, committer, err := db.TxContext(ctx)
   417  	if err != nil {
   418  		return err
   419  	}
   420  	defer committer.Close()
   421  
   422  	if err = removeRepositoryFromTeam(ctx, t, repo, true); err != nil {
   423  		return err
   424  	}
   425  
   426  	return committer.Commit()
   427  }
   428  
   429  // DeleteOwnerRepositoriesDirectly calls DeleteRepositoryDirectly for all repos of the given owner
   430  func DeleteOwnerRepositoriesDirectly(ctx context.Context, owner *user_model.User) error {
   431  	for {
   432  		repos, _, err := repo_model.GetUserRepositories(ctx, &repo_model.SearchRepoOptions{
   433  			ListOptions: db.ListOptions{
   434  				PageSize: repo_model.RepositoryListDefaultPageSize,
   435  				Page:     1,
   436  			},
   437  			Private: true,
   438  			OwnerID: owner.ID,
   439  			Actor:   owner,
   440  		})
   441  		if err != nil {
   442  			return fmt.Errorf("GetUserRepositories: %w", err)
   443  		}
   444  		if len(repos) == 0 {
   445  			break
   446  		}
   447  		for _, repo := range repos {
   448  			if err := DeleteRepositoryDirectly(ctx, owner, owner.ID, repo.ID); err != nil {
   449  				return fmt.Errorf("unable to delete repository %s for %s[%d]. Error: %w", repo.Name, owner.Name, owner.ID, err)
   450  			}
   451  		}
   452  	}
   453  	return nil
   454  }