gitea/services/repository/fork.go

274 lines
7.9 KiB
Go
Raw Normal View History

// Copyright 2019 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package repository
import (
"context"
"fmt"
"strings"
"time"
"code.gitea.io/gitea/models/db"
git_model "code.gitea.io/gitea/models/git"
repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/models/unit"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/gitrepo"
"code.gitea.io/gitea/modules/log"
repo_module "code.gitea.io/gitea/modules/repository"
"code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/util"
notify_service "code.gitea.io/gitea/services/notify"
"xorm.io/builder"
)
// ErrForkAlreadyExist represents a "ForkAlreadyExist" kind of error.
type ErrForkAlreadyExist struct {
Uname string
RepoName string
ForkName string
}
// IsErrForkAlreadyExist checks if an error is an ErrForkAlreadyExist.
func IsErrForkAlreadyExist(err error) bool {
_, ok := err.(ErrForkAlreadyExist)
return ok
}
func (err ErrForkAlreadyExist) Error() string {
return fmt.Sprintf("repository is already forked by user [uname: %s, repo path: %s, fork path: %s]", err.Uname, err.RepoName, err.ForkName)
}
func (err ErrForkAlreadyExist) Unwrap() error {
return util.ErrAlreadyExist
}
// ForkRepoOptions contains the fork repository options
type ForkRepoOptions struct {
BaseRepo *repo_model.Repository
Name string
Description string
SingleBranch string
}
// ForkRepository forks a repository
func ForkRepository(ctx context.Context, doer, owner *user_model.User, opts ForkRepoOptions) (*repo_model.Repository, error) {
if err := opts.BaseRepo.LoadOwner(ctx); err != nil {
return nil, err
}
if user_model.IsUserBlockedBy(ctx, doer, opts.BaseRepo.Owner.ID) {
return nil, user_model.ErrBlockedUser
}
// Fork is prohibited, if user has reached maximum limit of repositories
if !owner.CanForkRepo() {
return nil, repo_model.ErrReachLimitOfRepo{
Limit: owner.MaxRepoCreation,
}
}
forkedRepo, err := repo_model.GetUserFork(ctx, opts.BaseRepo.ID, owner.ID)
if err != nil {
return nil, err
}
if forkedRepo != nil {
return nil, ErrForkAlreadyExist{
Uname: owner.Name,
RepoName: opts.BaseRepo.FullName(),
ForkName: forkedRepo.FullName(),
}
}
defaultBranch := opts.BaseRepo.DefaultBranch
if opts.SingleBranch != "" {
defaultBranch = opts.SingleBranch
}
repo := &repo_model.Repository{
OwnerID: owner.ID,
Owner: owner,
OwnerName: owner.Name,
Name: opts.Name,
LowerName: strings.ToLower(opts.Name),
Description: opts.Description,
DefaultBranch: defaultBranch,
IsPrivate: opts.BaseRepo.IsPrivate || opts.BaseRepo.Owner.Visibility == structs.VisibleTypePrivate,
IsEmpty: opts.BaseRepo.IsEmpty,
IsFork: true,
ForkID: opts.BaseRepo.ID,
ObjectFormatName: opts.BaseRepo.ObjectFormatName,
}
oldRepoPath := opts.BaseRepo.RepoPath()
needsRollback := false
rollbackFn := func() {
if !needsRollback {
return
}
repoPath := repo_model.RepoPath(owner.Name, repo.Name)
if exists, _ := util.IsExist(repoPath); !exists {
return
}
// As the transaction will be failed and hence database changes will be destroyed we only need
// to delete the related repository on the filesystem
if errDelete := util.RemoveAll(repoPath); errDelete != nil {
log.Error("Failed to remove fork repo")
}
}
needsRollbackInPanic := true
defer func() {
panicErr := recover()
if panicErr == nil {
return
}
if needsRollbackInPanic {
rollbackFn()
}
panic(panicErr)
}()
err = db.WithTx(ctx, func(txCtx context.Context) error {
if err = CreateRepositoryByExample(txCtx, doer, owner, repo, false, true); err != nil {
return err
}
if err = repo_model.IncrementRepoForkNum(txCtx, opts.BaseRepo.ID); err != nil {
return err
}
// copy lfs files failure should not be ignored
if err = git_model.CopyLFS(txCtx, repo, opts.BaseRepo); err != nil {
return err
}
needsRollback = true
cloneCmd := git.NewCommand(txCtx, "clone", "--bare")
if opts.SingleBranch != "" {
cloneCmd.AddArguments("--single-branch", "--branch").AddDynamicArguments(opts.SingleBranch)
}
repoPath := repo_model.RepoPath(owner.Name, repo.Name)
if stdout, _, err := cloneCmd.AddDynamicArguments(oldRepoPath, repoPath).
SetDescription(fmt.Sprintf("ForkRepository(git clone): %s to %s", opts.BaseRepo.FullName(), repo.FullName())).
RunStdBytes(&git.RunOpts{Timeout: 10 * time.Minute}); err != nil {
log.Error("Fork Repository (git clone) Failed for %v (from %v):\nStdout: %s\nError: %v", repo, opts.BaseRepo, stdout, err)
return fmt.Errorf("git clone: %w", err)
}
if err := repo_module.CheckDaemonExportOK(txCtx, repo); err != nil {
return fmt.Errorf("checkDaemonExportOK: %w", err)
}
if stdout, _, err := git.NewCommand(txCtx, "update-server-info").
SetDescription(fmt.Sprintf("ForkRepository(git update-server-info): %s", repo.FullName())).
RunStdString(&git.RunOpts{Dir: repoPath}); err != nil {
log.Error("Fork Repository (git update-server-info) failed for %v:\nStdout: %s\nError: %v", repo, stdout, err)
return fmt.Errorf("git update-server-info: %w", err)
}
if err = repo_module.CreateDelegateHooks(repoPath); err != nil {
return fmt.Errorf("createDelegateHooks: %w", err)
}
gitRepo, err := gitrepo.OpenRepository(txCtx, repo)
if err != nil {
return fmt.Errorf("OpenRepository: %w", err)
}
defer gitRepo.Close()
_, err = repo_module.SyncRepoBranchesWithRepo(txCtx, repo, gitRepo, doer.ID)
return err
})
needsRollbackInPanic = false
if err != nil {
rollbackFn()
return nil, err
}
// even if below operations failed, it could be ignored. And they will be retried
if err := repo_module.UpdateRepoSize(ctx, repo); err != nil {
log.Error("Failed to update size for repository: %v", err)
}
if err := repo_model.CopyLanguageStat(ctx, opts.BaseRepo, repo); err != nil {
log.Error("Copy language stat from oldRepo failed: %v", err)
}
Support repo license (#24872) Close #278 Close #24076 ## Solutions: - Use [google/licenseclassifier](https://github.com/google/licenseclassifier/) Test result between [google/licensecheck](https://github.com/google/licensecheck) and [go-license-detector](https://github.com/go-enry/go-license-detector): https://github.com/go-gitea/gitea/pull/24872#issuecomment-1560361167 Test result between [google/licensecheck](https://github.com/google/licensecheck) and [google/licenseclassifier](https://github.com/google/licenseclassifier/): https://github.com/go-gitea/gitea/pull/24872#issuecomment-1576092178 - Generate License Convert Name List to avoid import license templates with same contents Gitea automatically get latest license data from[ spdx/license-list-data](https://github.com/spdx/license-list-data). But unfortunately, some license templates have same contents. #20915 [click here to see the list](https://github.com/go-gitea/gitea/pull/24872#issuecomment-1584141684) So we will generate a list of these license templates with same contents and create a new file to save the result when using `make generate-license`. (Need to decide the save path) - Save License info into a new table `repo_license` Can easily support searching repo by license in the future. ## Screen shot Single License: ![image](https://github.com/go-gitea/gitea/assets/18380374/41260bd7-0b4c-4038-8592-508706cffa9f) Multiple Licenses: ![image](https://github.com/go-gitea/gitea/assets/18380374/34ce2f73-7e18-446b-9b96-ecc4fb61bd70) Triggers: - [x] Push commit to default branch - [x] Create repo - [x] Mirror repo - [x] When Default Branch is changed, licenses should be updated Todo: - [x] Save Licenses info in to DB when there's a change to license file in the commit - [x] DB Migration - [x] A nominal test? - [x] Select which library to use(https://github.com/go-gitea/gitea/pull/24872#issuecomment-1560361167) - [x] API Support - [x] Add repo license table - ~Select license in settings if there are several licenses(Not recommended)~ - License board(later, not in this PR) ![image](https://github.com/go-gitea/gitea/assets/18380374/2c3c3bf8-bcc2-4c6d-8ce0-81d1a9733878) --------- Co-authored-by: silverwind <me@silverwind.io> Co-authored-by: Lunny Xiao <xiaolunwen@gmail.com> Co-authored-by: Denys Konovalov <kontakt@denyskon.de> Co-authored-by: delvh <dev.lh@web.de> Co-authored-by: KN4CK3R <admin@oldschoolhack.me> Co-authored-by: 6543 <6543@obermui.de> Co-authored-by: 6543 <m.huber@kithara.com> Co-authored-by: a1012112796 <1012112796@qq.com> Co-authored-by: techknowlogick <techknowlogick@gitea.com>
2024-10-01 21:25:08 +02:00
if err := repo_model.CopyLicense(ctx, opts.BaseRepo, repo); err != nil {
return nil, err
}
gitRepo, err := gitrepo.OpenRepository(ctx, repo)
if err != nil {
log.Error("Open created git repository failed: %v", err)
} else {
defer gitRepo.Close()
if err := repo_module.SyncReleasesWithTags(ctx, repo, gitRepo); err != nil {
log.Error("Sync releases from git tags failed: %v", err)
}
}
Adopt repositories (#12920) * Don't automatically delete repository files if they are present Prior to this PR Gitea would delete any repository files if they are present during creation or migration. This can in certain circumstances lead to data-loss and is slightly unpleasant. This PR provides a mechanism for Gitea to adopt repositories on creation and otherwise requires an explicit flag for deletion. PushCreate is slightly different - the create will cause adoption if that is allowed otherwise it will delete the data if that is allowed. Signed-off-by: Andrew Thornton <art27@cantab.net> * Update swagger Signed-off-by: Andrew Thornton <art27@cantab.net> * Fix tests and migrate overwrite Signed-off-by: Andrew Thornton <art27@cantab.net> * as per @lunny Only offer to adopt or overwrite if the user can do that. Allow the site administrator to adopt or overwrite in all circumstances Signed-off-by: Andrew Thornton <art27@cantab.net> * Use setting.Repository.DefaultBranch for the default branch Signed-off-by: Andrew Thornton <art27@cantab.net> * Always set setting.Repository.DefaultBranch Signed-off-by: Andrew Thornton <art27@cantab.net> * update swagger Signed-off-by: Andrew Thornton <art27@cantab.net> * update templates Signed-off-by: Andrew Thornton <art27@cantab.net> * ensure repo closed Signed-off-by: Andrew Thornton <art27@cantab.net> * Rewrite of adoption as per @6543 and @lunny Signed-off-by: Andrew Thornton <art27@cantab.net> * Apply suggestions from code review * update swagger Signed-off-by: Andrew Thornton <art27@cantab.net> * missing not Signed-off-by: Andrew Thornton <art27@cantab.net> * add modals and flash reporting Signed-off-by: Andrew Thornton <art27@cantab.net> * Make the unadopted page searchable Signed-off-by: Andrew Thornton <art27@cantab.net> * Add API Signed-off-by: Andrew Thornton <art27@cantab.net> * Fix swagger Signed-off-by: Andrew Thornton <art27@cantab.net> * fix swagger Signed-off-by: Andrew Thornton <art27@cantab.net> * Handle empty and non-master branched repositories Signed-off-by: Andrew Thornton <art27@cantab.net> * placate lint Signed-off-by: Andrew Thornton <art27@cantab.net> * remove commented out code Signed-off-by: Andrew Thornton <art27@cantab.net> Co-authored-by: techknowlogick <techknowlogick@gitea.io>
2020-09-25 06:09:23 +02:00
notify_service.ForkRepository(ctx, doer, opts.BaseRepo, repo)
Adopt repositories (#12920) * Don't automatically delete repository files if they are present Prior to this PR Gitea would delete any repository files if they are present during creation or migration. This can in certain circumstances lead to data-loss and is slightly unpleasant. This PR provides a mechanism for Gitea to adopt repositories on creation and otherwise requires an explicit flag for deletion. PushCreate is slightly different - the create will cause adoption if that is allowed otherwise it will delete the data if that is allowed. Signed-off-by: Andrew Thornton <art27@cantab.net> * Update swagger Signed-off-by: Andrew Thornton <art27@cantab.net> * Fix tests and migrate overwrite Signed-off-by: Andrew Thornton <art27@cantab.net> * as per @lunny Only offer to adopt or overwrite if the user can do that. Allow the site administrator to adopt or overwrite in all circumstances Signed-off-by: Andrew Thornton <art27@cantab.net> * Use setting.Repository.DefaultBranch for the default branch Signed-off-by: Andrew Thornton <art27@cantab.net> * Always set setting.Repository.DefaultBranch Signed-off-by: Andrew Thornton <art27@cantab.net> * update swagger Signed-off-by: Andrew Thornton <art27@cantab.net> * update templates Signed-off-by: Andrew Thornton <art27@cantab.net> * ensure repo closed Signed-off-by: Andrew Thornton <art27@cantab.net> * Rewrite of adoption as per @6543 and @lunny Signed-off-by: Andrew Thornton <art27@cantab.net> * Apply suggestions from code review * update swagger Signed-off-by: Andrew Thornton <art27@cantab.net> * missing not Signed-off-by: Andrew Thornton <art27@cantab.net> * add modals and flash reporting Signed-off-by: Andrew Thornton <art27@cantab.net> * Make the unadopted page searchable Signed-off-by: Andrew Thornton <art27@cantab.net> * Add API Signed-off-by: Andrew Thornton <art27@cantab.net> * Fix swagger Signed-off-by: Andrew Thornton <art27@cantab.net> * fix swagger Signed-off-by: Andrew Thornton <art27@cantab.net> * Handle empty and non-master branched repositories Signed-off-by: Andrew Thornton <art27@cantab.net> * placate lint Signed-off-by: Andrew Thornton <art27@cantab.net> * remove commented out code Signed-off-by: Andrew Thornton <art27@cantab.net> Co-authored-by: techknowlogick <techknowlogick@gitea.io>
2020-09-25 06:09:23 +02:00
return repo, nil
}
// ConvertForkToNormalRepository convert the provided repo from a forked repo to normal repo
func ConvertForkToNormalRepository(ctx context.Context, repo *repo_model.Repository) error {
err := db.WithTx(ctx, func(ctx context.Context) error {
repo, err := repo_model.GetRepositoryByID(ctx, repo.ID)
if err != nil {
return err
}
if !repo.IsFork {
return nil
}
if err := repo_model.DecrementRepoForkNum(ctx, repo.ForkID); err != nil {
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)
return err
}
repo.IsFork = false
repo.ForkID = 0
if err := repo_module.UpdateRepository(ctx, repo, false); err != nil {
log.Error("Unable to update repository %-v whilst converting from fork. Error: %v", repo, err)
return err
}
return nil
})
return err
}
type findForksOptions struct {
db.ListOptions
RepoID int64
Doer *user_model.User
}
func (opts findForksOptions) ToConds() builder.Cond {
return builder.Eq{"fork_id": opts.RepoID}.And(
repo_model.AccessibleRepositoryCondition(opts.Doer, unit.TypeInvalid),
)
}
// FindForks returns all the forks of the repository
func FindForks(ctx context.Context, repo *repo_model.Repository, doer *user_model.User, listOptions db.ListOptions) ([]*repo_model.Repository, int64, error) {
return db.FindAndCount[repo_model.Repository](ctx, findForksOptions{
ListOptions: listOptions,
RepoID: repo.ID,
Doer: doer,
})
}