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