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

     1  // Copyright 2019 The Gitea Authors. All rights reserved.
     2  // SPDX-License-Identifier: MIT
     3  
     4  package repository
     5  
     6  import (
     7  	"context"
     8  	"fmt"
     9  	"os"
    10  	"strings"
    11  
    12  	"code.gitea.io/gitea/models"
    13  	"code.gitea.io/gitea/models/db"
    14  	issues_model "code.gitea.io/gitea/models/issues"
    15  	"code.gitea.io/gitea/models/organization"
    16  	"code.gitea.io/gitea/models/perm"
    17  	access_model "code.gitea.io/gitea/models/perm/access"
    18  	project_model "code.gitea.io/gitea/models/project"
    19  	repo_model "code.gitea.io/gitea/models/repo"
    20  	user_model "code.gitea.io/gitea/models/user"
    21  	"code.gitea.io/gitea/modules/log"
    22  	repo_module "code.gitea.io/gitea/modules/repository"
    23  	"code.gitea.io/gitea/modules/sync"
    24  	"code.gitea.io/gitea/modules/util"
    25  	notify_service "code.gitea.io/gitea/services/notify"
    26  )
    27  
    28  // repoWorkingPool represents a working pool to order the parallel changes to the same repository
    29  // TODO: use clustered lock (unique queue? or *abuse* cache)
    30  var repoWorkingPool = sync.NewExclusivePool()
    31  
    32  // TransferOwnership transfers all corresponding setting from old user to new one.
    33  func TransferOwnership(ctx context.Context, doer, newOwner *user_model.User, repo *repo_model.Repository, teams []*organization.Team) error {
    34  	if err := repo.LoadOwner(ctx); err != nil {
    35  		return err
    36  	}
    37  	for _, team := range teams {
    38  		if newOwner.ID != team.OrgID {
    39  			return fmt.Errorf("team %d does not belong to organization", team.ID)
    40  		}
    41  	}
    42  
    43  	oldOwner := repo.Owner
    44  
    45  	repoWorkingPool.CheckIn(fmt.Sprint(repo.ID))
    46  	if err := transferOwnership(ctx, doer, newOwner.Name, repo); err != nil {
    47  		repoWorkingPool.CheckOut(fmt.Sprint(repo.ID))
    48  		return err
    49  	}
    50  	repoWorkingPool.CheckOut(fmt.Sprint(repo.ID))
    51  
    52  	newRepo, err := repo_model.GetRepositoryByID(ctx, repo.ID)
    53  	if err != nil {
    54  		return err
    55  	}
    56  
    57  	for _, team := range teams {
    58  		if err := models.AddRepository(ctx, team, newRepo); err != nil {
    59  			return err
    60  		}
    61  	}
    62  
    63  	notify_service.TransferRepository(ctx, doer, repo, oldOwner.Name)
    64  
    65  	return nil
    66  }
    67  
    68  // transferOwnership transfers all corresponding repository items from old user to new one.
    69  func transferOwnership(ctx context.Context, doer *user_model.User, newOwnerName string, repo *repo_model.Repository) (err error) {
    70  	repoRenamed := false
    71  	wikiRenamed := false
    72  	oldOwnerName := doer.Name
    73  
    74  	defer func() {
    75  		if !repoRenamed && !wikiRenamed {
    76  			return
    77  		}
    78  
    79  		recoverErr := recover()
    80  		if err == nil && recoverErr == nil {
    81  			return
    82  		}
    83  
    84  		if repoRenamed {
    85  			if err := util.Rename(repo_model.RepoPath(newOwnerName, repo.Name), repo_model.RepoPath(oldOwnerName, repo.Name)); err != nil {
    86  				log.Critical("Unable to move repository %s/%s directory from %s back to correct place %s: %v", oldOwnerName, repo.Name,
    87  					repo_model.RepoPath(newOwnerName, repo.Name), repo_model.RepoPath(oldOwnerName, repo.Name), err)
    88  			}
    89  		}
    90  
    91  		if wikiRenamed {
    92  			if err := util.Rename(repo_model.WikiPath(newOwnerName, repo.Name), repo_model.WikiPath(oldOwnerName, repo.Name)); err != nil {
    93  				log.Critical("Unable to move wiki for repository %s/%s directory from %s back to correct place %s: %v", oldOwnerName, repo.Name,
    94  					repo_model.WikiPath(newOwnerName, repo.Name), repo_model.WikiPath(oldOwnerName, repo.Name), err)
    95  			}
    96  		}
    97  
    98  		if recoverErr != nil {
    99  			log.Error("Panic within TransferOwnership: %v\n%s", recoverErr, log.Stack(2))
   100  			panic(recoverErr)
   101  		}
   102  	}()
   103  
   104  	ctx, committer, err := db.TxContext(ctx)
   105  	if err != nil {
   106  		return err
   107  	}
   108  	defer committer.Close()
   109  
   110  	sess := db.GetEngine(ctx)
   111  
   112  	newOwner, err := user_model.GetUserByName(ctx, newOwnerName)
   113  	if err != nil {
   114  		return fmt.Errorf("get new owner '%s': %w", newOwnerName, err)
   115  	}
   116  	newOwnerName = newOwner.Name // ensure capitalisation matches
   117  
   118  	// Check if new owner has repository with same name.
   119  	if has, err := repo_model.IsRepositoryModelOrDirExist(ctx, newOwner, repo.Name); err != nil {
   120  		return fmt.Errorf("IsRepositoryExist: %w", err)
   121  	} else if has {
   122  		return repo_model.ErrRepoAlreadyExist{
   123  			Uname: newOwnerName,
   124  			Name:  repo.Name,
   125  		}
   126  	}
   127  
   128  	oldOwner := repo.Owner
   129  	oldOwnerName = oldOwner.Name
   130  
   131  	// Note: we have to set value here to make sure recalculate accesses is based on
   132  	// new owner.
   133  	repo.OwnerID = newOwner.ID
   134  	repo.Owner = newOwner
   135  	repo.OwnerName = newOwner.Name
   136  
   137  	// Update repository.
   138  	if _, err := sess.ID(repo.ID).Update(repo); err != nil {
   139  		return fmt.Errorf("update owner: %w", err)
   140  	}
   141  
   142  	// Remove redundant collaborators.
   143  	collaborators, _, err := repo_model.GetCollaborators(ctx, &repo_model.FindCollaborationOptions{RepoID: repo.ID})
   144  	if err != nil {
   145  		return fmt.Errorf("GetCollaborators: %w", err)
   146  	}
   147  
   148  	// Dummy object.
   149  	collaboration := &repo_model.Collaboration{RepoID: repo.ID}
   150  	for _, c := range collaborators {
   151  		if c.IsGhost() {
   152  			collaboration.ID = c.Collaboration.ID
   153  			if _, err := sess.Delete(collaboration); err != nil {
   154  				return fmt.Errorf("remove collaborator '%d': %w", c.ID, err)
   155  			}
   156  			collaboration.ID = 0
   157  		}
   158  
   159  		if c.ID != newOwner.ID {
   160  			isMember, err := organization.IsOrganizationMember(ctx, newOwner.ID, c.ID)
   161  			if err != nil {
   162  				return fmt.Errorf("IsOrgMember: %w", err)
   163  			} else if !isMember {
   164  				continue
   165  			}
   166  		}
   167  		collaboration.UserID = c.ID
   168  		if _, err := sess.Delete(collaboration); err != nil {
   169  			return fmt.Errorf("remove collaborator '%d': %w", c.ID, err)
   170  		}
   171  		collaboration.UserID = 0
   172  	}
   173  
   174  	// Remove old team-repository relations.
   175  	if oldOwner.IsOrganization() {
   176  		if err := organization.RemoveOrgRepo(ctx, oldOwner.ID, repo.ID); err != nil {
   177  			return fmt.Errorf("removeOrgRepo: %w", err)
   178  		}
   179  	}
   180  
   181  	// Remove project's issues that belong to old organization's projects
   182  	if oldOwner.IsOrganization() {
   183  		projects, err := project_model.GetAllProjectsIDsByOwnerIDAndType(ctx, oldOwner.ID, project_model.TypeOrganization)
   184  		if err != nil {
   185  			return fmt.Errorf("Unable to find old org projects: %w", err)
   186  		}
   187  		issues, err := issues_model.GetIssueIDsByRepoID(ctx, repo.ID)
   188  		if err != nil {
   189  			return fmt.Errorf("Unable to find repo's issues: %w", err)
   190  		}
   191  		err = project_model.DeleteAllProjectIssueByIssueIDsAndProjectIDs(ctx, issues, projects)
   192  		if err != nil {
   193  			return fmt.Errorf("Unable to delete project's issues: %w", err)
   194  		}
   195  	}
   196  
   197  	if newOwner.IsOrganization() {
   198  		teams, err := organization.FindOrgTeams(ctx, newOwner.ID)
   199  		if err != nil {
   200  			return fmt.Errorf("LoadTeams: %w", err)
   201  		}
   202  		for _, t := range teams {
   203  			if t.IncludesAllRepositories {
   204  				if err := models.AddRepository(ctx, t, repo); err != nil {
   205  					return fmt.Errorf("AddRepository: %w", err)
   206  				}
   207  			}
   208  		}
   209  	} else if err := access_model.RecalculateAccesses(ctx, repo); err != nil {
   210  		// Organization called this in addRepository method.
   211  		return fmt.Errorf("recalculateAccesses: %w", err)
   212  	}
   213  
   214  	// Update repository count.
   215  	if _, err := sess.Exec("UPDATE `user` SET num_repos=num_repos+1 WHERE id=?", newOwner.ID); err != nil {
   216  		return fmt.Errorf("increase new owner repository count: %w", err)
   217  	} else if _, err := sess.Exec("UPDATE `user` SET num_repos=num_repos-1 WHERE id=?", oldOwner.ID); err != nil {
   218  		return fmt.Errorf("decrease old owner repository count: %w", err)
   219  	}
   220  
   221  	if err := repo_model.WatchRepo(ctx, doer, repo, true); err != nil {
   222  		return fmt.Errorf("watchRepo: %w", err)
   223  	}
   224  
   225  	// Remove watch for organization.
   226  	if oldOwner.IsOrganization() {
   227  		if err := repo_model.WatchRepo(ctx, oldOwner, repo, false); err != nil {
   228  			return fmt.Errorf("watchRepo [false]: %w", err)
   229  		}
   230  	}
   231  
   232  	// Delete labels that belong to the old organization and comments that added these labels
   233  	if oldOwner.IsOrganization() {
   234  		if _, err := sess.Exec(`DELETE FROM issue_label WHERE issue_label.id IN (
   235  			SELECT il_too.id FROM (
   236  				SELECT il_too_too.id
   237  					FROM issue_label AS il_too_too
   238  						INNER JOIN label ON il_too_too.label_id = label.id
   239  						INNER JOIN issue on issue.id = il_too_too.issue_id
   240  					WHERE
   241  						issue.repo_id = ? AND ((label.org_id = 0 AND issue.repo_id != label.repo_id) OR (label.repo_id = 0 AND label.org_id != ?))
   242  		) AS il_too )`, repo.ID, newOwner.ID); err != nil {
   243  			return fmt.Errorf("Unable to remove old org labels: %w", err)
   244  		}
   245  
   246  		if _, err := sess.Exec(`DELETE FROM comment WHERE comment.id IN (
   247  			SELECT il_too.id FROM (
   248  				SELECT com.id
   249  					FROM comment AS com
   250  						INNER JOIN label ON com.label_id = label.id
   251  						INNER JOIN issue ON issue.id = com.issue_id
   252  					WHERE
   253  						com.type = ? AND issue.repo_id = ? AND ((label.org_id = 0 AND issue.repo_id != label.repo_id) OR (label.repo_id = 0 AND label.org_id != ?))
   254  		) AS il_too)`, issues_model.CommentTypeLabel, repo.ID, newOwner.ID); err != nil {
   255  			return fmt.Errorf("Unable to remove old org label comments: %w", err)
   256  		}
   257  	}
   258  
   259  	// Rename remote repository to new path and delete local copy.
   260  	dir := user_model.UserPath(newOwner.Name)
   261  
   262  	if err := os.MkdirAll(dir, os.ModePerm); err != nil {
   263  		return fmt.Errorf("Failed to create dir %s: %w", dir, err)
   264  	}
   265  
   266  	if err := util.Rename(repo_model.RepoPath(oldOwner.Name, repo.Name), repo_model.RepoPath(newOwner.Name, repo.Name)); err != nil {
   267  		return fmt.Errorf("rename repository directory: %w", err)
   268  	}
   269  	repoRenamed = true
   270  
   271  	// Rename remote wiki repository to new path and delete local copy.
   272  	wikiPath := repo_model.WikiPath(oldOwner.Name, repo.Name)
   273  
   274  	if isExist, err := util.IsExist(wikiPath); err != nil {
   275  		log.Error("Unable to check if %s exists. Error: %v", wikiPath, err)
   276  		return err
   277  	} else if isExist {
   278  		if err := util.Rename(wikiPath, repo_model.WikiPath(newOwner.Name, repo.Name)); err != nil {
   279  			return fmt.Errorf("rename repository wiki: %w", err)
   280  		}
   281  		wikiRenamed = true
   282  	}
   283  
   284  	if err := models.DeleteRepositoryTransfer(ctx, repo.ID); err != nil {
   285  		return fmt.Errorf("deleteRepositoryTransfer: %w", err)
   286  	}
   287  	repo.Status = repo_model.RepositoryReady
   288  	if err := repo_model.UpdateRepositoryCols(ctx, repo, "status"); err != nil {
   289  		return err
   290  	}
   291  
   292  	// If there was previously a redirect at this location, remove it.
   293  	if err := repo_model.DeleteRedirect(ctx, newOwner.ID, repo.Name); err != nil {
   294  		return fmt.Errorf("delete repo redirect: %w", err)
   295  	}
   296  
   297  	if err := repo_model.NewRedirect(ctx, oldOwner.ID, repo.ID, repo.Name, repo.Name); err != nil {
   298  		return fmt.Errorf("repo_model.NewRedirect: %w", err)
   299  	}
   300  
   301  	return committer.Commit()
   302  }
   303  
   304  // changeRepositoryName changes all corresponding setting from old repository name to new one.
   305  func changeRepositoryName(ctx context.Context, doer *user_model.User, repo *repo_model.Repository, newRepoName string) (err error) {
   306  	oldRepoName := repo.Name
   307  	newRepoName = strings.ToLower(newRepoName)
   308  	if err = repo_model.IsUsableRepoName(newRepoName); err != nil {
   309  		return err
   310  	}
   311  
   312  	if err := repo.LoadOwner(ctx); err != nil {
   313  		return err
   314  	}
   315  
   316  	has, err := repo_model.IsRepositoryModelOrDirExist(ctx, repo.Owner, newRepoName)
   317  	if err != nil {
   318  		return fmt.Errorf("IsRepositoryExist: %w", err)
   319  	} else if has {
   320  		return repo_model.ErrRepoAlreadyExist{
   321  			Uname: repo.Owner.Name,
   322  			Name:  newRepoName,
   323  		}
   324  	}
   325  
   326  	newRepoPath := repo_model.RepoPath(repo.Owner.Name, newRepoName)
   327  	if err = util.Rename(repo.RepoPath(), newRepoPath); err != nil {
   328  		return fmt.Errorf("rename repository directory: %w", err)
   329  	}
   330  
   331  	wikiPath := repo.WikiPath()
   332  	isExist, err := util.IsExist(wikiPath)
   333  	if err != nil {
   334  		log.Error("Unable to check if %s exists. Error: %v", wikiPath, err)
   335  		return err
   336  	}
   337  	if isExist {
   338  		if err = util.Rename(wikiPath, repo_model.WikiPath(repo.Owner.Name, newRepoName)); err != nil {
   339  			return fmt.Errorf("rename repository wiki: %w", err)
   340  		}
   341  	}
   342  
   343  	ctx, committer, err := db.TxContext(ctx)
   344  	if err != nil {
   345  		return err
   346  	}
   347  	defer committer.Close()
   348  
   349  	if err := repo_model.NewRedirect(ctx, repo.Owner.ID, repo.ID, oldRepoName, newRepoName); err != nil {
   350  		return err
   351  	}
   352  
   353  	return committer.Commit()
   354  }
   355  
   356  // ChangeRepositoryName changes all corresponding setting from old repository name to new one.
   357  func ChangeRepositoryName(ctx context.Context, doer *user_model.User, repo *repo_model.Repository, newRepoName string) error {
   358  	log.Trace("ChangeRepositoryName: %s/%s -> %s", doer.Name, repo.Name, newRepoName)
   359  
   360  	oldRepoName := repo.Name
   361  
   362  	// Change repository directory name. We must lock the local copy of the
   363  	// repo so that we can atomically rename the repo path and updates the
   364  	// local copy's origin accordingly.
   365  
   366  	repoWorkingPool.CheckIn(fmt.Sprint(repo.ID))
   367  	if err := changeRepositoryName(ctx, doer, repo, newRepoName); err != nil {
   368  		repoWorkingPool.CheckOut(fmt.Sprint(repo.ID))
   369  		return err
   370  	}
   371  	repoWorkingPool.CheckOut(fmt.Sprint(repo.ID))
   372  
   373  	repo.Name = newRepoName
   374  	notify_service.RenameRepository(ctx, doer, repo, oldRepoName)
   375  
   376  	return nil
   377  }
   378  
   379  // StartRepositoryTransfer transfer a repo from one owner to a new one.
   380  // it make repository into pending transfer state, if doer can not create repo for new owner.
   381  func StartRepositoryTransfer(ctx context.Context, doer, newOwner *user_model.User, repo *repo_model.Repository, teams []*organization.Team) error {
   382  	if err := models.TestRepositoryReadyForTransfer(repo.Status); err != nil {
   383  		return err
   384  	}
   385  
   386  	// Admin is always allowed to transfer || user transfer repo back to his account
   387  	if doer.IsAdmin || doer.ID == newOwner.ID {
   388  		return TransferOwnership(ctx, doer, newOwner, repo, teams)
   389  	}
   390  
   391  	if user_model.IsUserBlockedBy(ctx, doer, newOwner.ID) {
   392  		return user_model.ErrBlockedUser
   393  	}
   394  
   395  	// If new owner is an org and user can create repos he can transfer directly too
   396  	if newOwner.IsOrganization() {
   397  		allowed, err := organization.CanCreateOrgRepo(ctx, newOwner.ID, doer.ID)
   398  		if err != nil {
   399  			return err
   400  		}
   401  		if allowed {
   402  			return TransferOwnership(ctx, doer, newOwner, repo, teams)
   403  		}
   404  	}
   405  
   406  	// In case the new owner would not have sufficient access to the repo, give access rights for read
   407  	hasAccess, err := access_model.HasAnyUnitAccess(ctx, newOwner.ID, repo)
   408  	if err != nil {
   409  		return err
   410  	}
   411  	if !hasAccess {
   412  		if err := repo_module.AddCollaborator(ctx, repo, newOwner); err != nil {
   413  			return err
   414  		}
   415  		if err := repo_model.ChangeCollaborationAccessMode(ctx, repo, newOwner.ID, perm.AccessModeRead); err != nil {
   416  			return err
   417  		}
   418  	}
   419  
   420  	// Make repo as pending for transfer
   421  	repo.Status = repo_model.RepositoryPendingTransfer
   422  	if err := models.CreatePendingRepositoryTransfer(ctx, doer, newOwner, repo.ID, teams); err != nil {
   423  		return err
   424  	}
   425  
   426  	// notify users who are able to accept / reject transfer
   427  	notify_service.RepoPendingTransfer(ctx, doer, newOwner, repo)
   428  
   429  	return nil
   430  }
   431  
   432  // CancelRepositoryTransfer marks the repository as ready and remove pending transfer entry,
   433  // thus cancel the transfer process.
   434  func CancelRepositoryTransfer(ctx context.Context, repo *repo_model.Repository) error {
   435  	ctx, committer, err := db.TxContext(ctx)
   436  	if err != nil {
   437  		return err
   438  	}
   439  	defer committer.Close()
   440  
   441  	repo.Status = repo_model.RepositoryReady
   442  	if err := repo_model.UpdateRepositoryCols(ctx, repo, "status"); err != nil {
   443  		return err
   444  	}
   445  
   446  	if err := models.DeleteRepositoryTransfer(ctx, repo.ID); err != nil {
   447  		return err
   448  	}
   449  
   450  	return committer.Commit()
   451  }