code.gitea.io/gitea@v1.22.3/services/repository/fork.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  	"strings"
    10  	"time"
    11  
    12  	"code.gitea.io/gitea/models/db"
    13  	git_model "code.gitea.io/gitea/models/git"
    14  	repo_model "code.gitea.io/gitea/models/repo"
    15  	user_model "code.gitea.io/gitea/models/user"
    16  	"code.gitea.io/gitea/modules/git"
    17  	"code.gitea.io/gitea/modules/gitrepo"
    18  	"code.gitea.io/gitea/modules/log"
    19  	repo_module "code.gitea.io/gitea/modules/repository"
    20  	"code.gitea.io/gitea/modules/structs"
    21  	"code.gitea.io/gitea/modules/util"
    22  	notify_service "code.gitea.io/gitea/services/notify"
    23  )
    24  
    25  // ErrForkAlreadyExist represents a "ForkAlreadyExist" kind of error.
    26  type ErrForkAlreadyExist struct {
    27  	Uname    string
    28  	RepoName string
    29  	ForkName string
    30  }
    31  
    32  // IsErrForkAlreadyExist checks if an error is an ErrForkAlreadyExist.
    33  func IsErrForkAlreadyExist(err error) bool {
    34  	_, ok := err.(ErrForkAlreadyExist)
    35  	return ok
    36  }
    37  
    38  func (err ErrForkAlreadyExist) Error() string {
    39  	return fmt.Sprintf("repository is already forked by user [uname: %s, repo path: %s, fork path: %s]", err.Uname, err.RepoName, err.ForkName)
    40  }
    41  
    42  func (err ErrForkAlreadyExist) Unwrap() error {
    43  	return util.ErrAlreadyExist
    44  }
    45  
    46  // ForkRepoOptions contains the fork repository options
    47  type ForkRepoOptions struct {
    48  	BaseRepo     *repo_model.Repository
    49  	Name         string
    50  	Description  string
    51  	SingleBranch string
    52  }
    53  
    54  // ForkRepository forks a repository
    55  func ForkRepository(ctx context.Context, doer, owner *user_model.User, opts ForkRepoOptions) (*repo_model.Repository, error) {
    56  	if err := opts.BaseRepo.LoadOwner(ctx); err != nil {
    57  		return nil, err
    58  	}
    59  
    60  	if user_model.IsUserBlockedBy(ctx, doer, opts.BaseRepo.Owner.ID) {
    61  		return nil, user_model.ErrBlockedUser
    62  	}
    63  
    64  	// Fork is prohibited, if user has reached maximum limit of repositories
    65  	if !owner.CanForkRepo() {
    66  		return nil, repo_model.ErrReachLimitOfRepo{
    67  			Limit: owner.MaxRepoCreation,
    68  		}
    69  	}
    70  
    71  	forkedRepo, err := repo_model.GetUserFork(ctx, opts.BaseRepo.ID, owner.ID)
    72  	if err != nil {
    73  		return nil, err
    74  	}
    75  	if forkedRepo != nil {
    76  		return nil, ErrForkAlreadyExist{
    77  			Uname:    owner.Name,
    78  			RepoName: opts.BaseRepo.FullName(),
    79  			ForkName: forkedRepo.FullName(),
    80  		}
    81  	}
    82  
    83  	defaultBranch := opts.BaseRepo.DefaultBranch
    84  	if opts.SingleBranch != "" {
    85  		defaultBranch = opts.SingleBranch
    86  	}
    87  	repo := &repo_model.Repository{
    88  		OwnerID:          owner.ID,
    89  		Owner:            owner,
    90  		OwnerName:        owner.Name,
    91  		Name:             opts.Name,
    92  		LowerName:        strings.ToLower(opts.Name),
    93  		Description:      opts.Description,
    94  		DefaultBranch:    defaultBranch,
    95  		IsPrivate:        opts.BaseRepo.IsPrivate || opts.BaseRepo.Owner.Visibility == structs.VisibleTypePrivate,
    96  		IsEmpty:          opts.BaseRepo.IsEmpty,
    97  		IsFork:           true,
    98  		ForkID:           opts.BaseRepo.ID,
    99  		ObjectFormatName: opts.BaseRepo.ObjectFormatName,
   100  	}
   101  
   102  	oldRepoPath := opts.BaseRepo.RepoPath()
   103  
   104  	needsRollback := false
   105  	rollbackFn := func() {
   106  		if !needsRollback {
   107  			return
   108  		}
   109  
   110  		repoPath := repo_model.RepoPath(owner.Name, repo.Name)
   111  
   112  		if exists, _ := util.IsExist(repoPath); !exists {
   113  			return
   114  		}
   115  
   116  		// As the transaction will be failed and hence database changes will be destroyed we only need
   117  		// to delete the related repository on the filesystem
   118  		if errDelete := util.RemoveAll(repoPath); errDelete != nil {
   119  			log.Error("Failed to remove fork repo")
   120  		}
   121  	}
   122  
   123  	needsRollbackInPanic := true
   124  	defer func() {
   125  		panicErr := recover()
   126  		if panicErr == nil {
   127  			return
   128  		}
   129  
   130  		if needsRollbackInPanic {
   131  			rollbackFn()
   132  		}
   133  		panic(panicErr)
   134  	}()
   135  
   136  	err = db.WithTx(ctx, func(txCtx context.Context) error {
   137  		if err = repo_module.CreateRepositoryByExample(txCtx, doer, owner, repo, false, true); err != nil {
   138  			return err
   139  		}
   140  
   141  		if err = repo_model.IncrementRepoForkNum(txCtx, opts.BaseRepo.ID); err != nil {
   142  			return err
   143  		}
   144  
   145  		// copy lfs files failure should not be ignored
   146  		if err = git_model.CopyLFS(txCtx, repo, opts.BaseRepo); err != nil {
   147  			return err
   148  		}
   149  
   150  		needsRollback = true
   151  
   152  		cloneCmd := git.NewCommand(txCtx, "clone", "--bare")
   153  		if opts.SingleBranch != "" {
   154  			cloneCmd.AddArguments("--single-branch", "--branch").AddDynamicArguments(opts.SingleBranch)
   155  		}
   156  		repoPath := repo_model.RepoPath(owner.Name, repo.Name)
   157  		if stdout, _, err := cloneCmd.AddDynamicArguments(oldRepoPath, repoPath).
   158  			SetDescription(fmt.Sprintf("ForkRepository(git clone): %s to %s", opts.BaseRepo.FullName(), repo.FullName())).
   159  			RunStdBytes(&git.RunOpts{Timeout: 10 * time.Minute}); err != nil {
   160  			log.Error("Fork Repository (git clone) Failed for %v (from %v):\nStdout: %s\nError: %v", repo, opts.BaseRepo, stdout, err)
   161  			return fmt.Errorf("git clone: %w", err)
   162  		}
   163  
   164  		if err := repo_module.CheckDaemonExportOK(txCtx, repo); err != nil {
   165  			return fmt.Errorf("checkDaemonExportOK: %w", err)
   166  		}
   167  
   168  		if stdout, _, err := git.NewCommand(txCtx, "update-server-info").
   169  			SetDescription(fmt.Sprintf("ForkRepository(git update-server-info): %s", repo.FullName())).
   170  			RunStdString(&git.RunOpts{Dir: repoPath}); err != nil {
   171  			log.Error("Fork Repository (git update-server-info) failed for %v:\nStdout: %s\nError: %v", repo, stdout, err)
   172  			return fmt.Errorf("git update-server-info: %w", err)
   173  		}
   174  
   175  		if err = repo_module.CreateDelegateHooks(repoPath); err != nil {
   176  			return fmt.Errorf("createDelegateHooks: %w", err)
   177  		}
   178  
   179  		gitRepo, err := gitrepo.OpenRepository(txCtx, repo)
   180  		if err != nil {
   181  			return fmt.Errorf("OpenRepository: %w", err)
   182  		}
   183  		defer gitRepo.Close()
   184  
   185  		_, err = repo_module.SyncRepoBranchesWithRepo(txCtx, repo, gitRepo, doer.ID)
   186  		return err
   187  	})
   188  	needsRollbackInPanic = false
   189  	if err != nil {
   190  		rollbackFn()
   191  		return nil, err
   192  	}
   193  
   194  	// even if below operations failed, it could be ignored. And they will be retried
   195  	if err := repo_module.UpdateRepoSize(ctx, repo); err != nil {
   196  		log.Error("Failed to update size for repository: %v", err)
   197  	}
   198  	if err := repo_model.CopyLanguageStat(ctx, opts.BaseRepo, repo); err != nil {
   199  		log.Error("Copy language stat from oldRepo failed: %v", err)
   200  	}
   201  
   202  	gitRepo, err := gitrepo.OpenRepository(ctx, repo)
   203  	if err != nil {
   204  		log.Error("Open created git repository failed: %v", err)
   205  	} else {
   206  		defer gitRepo.Close()
   207  		if err := repo_module.SyncReleasesWithTags(ctx, repo, gitRepo); err != nil {
   208  			log.Error("Sync releases from git tags failed: %v", err)
   209  		}
   210  	}
   211  
   212  	notify_service.ForkRepository(ctx, doer, opts.BaseRepo, repo)
   213  
   214  	return repo, nil
   215  }
   216  
   217  // ConvertForkToNormalRepository convert the provided repo from a forked repo to normal repo
   218  func ConvertForkToNormalRepository(ctx context.Context, repo *repo_model.Repository) error {
   219  	err := db.WithTx(ctx, func(ctx context.Context) error {
   220  		repo, err := repo_model.GetRepositoryByID(ctx, repo.ID)
   221  		if err != nil {
   222  			return err
   223  		}
   224  
   225  		if !repo.IsFork {
   226  			return nil
   227  		}
   228  
   229  		if err := repo_model.DecrementRepoForkNum(ctx, repo.ForkID); err != nil {
   230  			log.Error("Unable to decrement repo fork num for old root repo %d of repository %-v whilst converting from fork. Error: %v", repo.ForkID, repo, err)
   231  			return err
   232  		}
   233  
   234  		repo.IsFork = false
   235  		repo.ForkID = 0
   236  
   237  		if err := repo_module.UpdateRepository(ctx, repo, false); err != nil {
   238  			log.Error("Unable to update repository %-v whilst converting from fork. Error: %v", repo, err)
   239  			return err
   240  		}
   241  
   242  		return nil
   243  	})
   244  
   245  	return err
   246  }