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

     1  // Copyright 2020 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  	"path"
    11  	"path/filepath"
    12  	"strings"
    13  
    14  	"code.gitea.io/gitea/models/db"
    15  	git_model "code.gitea.io/gitea/models/git"
    16  	repo_model "code.gitea.io/gitea/models/repo"
    17  	user_model "code.gitea.io/gitea/models/user"
    18  	"code.gitea.io/gitea/modules/container"
    19  	"code.gitea.io/gitea/modules/git"
    20  	"code.gitea.io/gitea/modules/gitrepo"
    21  	"code.gitea.io/gitea/modules/log"
    22  	"code.gitea.io/gitea/modules/optional"
    23  	repo_module "code.gitea.io/gitea/modules/repository"
    24  	"code.gitea.io/gitea/modules/setting"
    25  	"code.gitea.io/gitea/modules/util"
    26  	notify_service "code.gitea.io/gitea/services/notify"
    27  
    28  	"github.com/gobwas/glob"
    29  )
    30  
    31  // AdoptRepository adopts pre-existing repository files for the user/organization.
    32  func AdoptRepository(ctx context.Context, doer, u *user_model.User, opts CreateRepoOptions) (*repo_model.Repository, error) {
    33  	if !doer.IsAdmin && !u.CanCreateRepo() {
    34  		return nil, repo_model.ErrReachLimitOfRepo{
    35  			Limit: u.MaxRepoCreation,
    36  		}
    37  	}
    38  
    39  	repo := &repo_model.Repository{
    40  		OwnerID:                         u.ID,
    41  		Owner:                           u,
    42  		OwnerName:                       u.Name,
    43  		Name:                            opts.Name,
    44  		LowerName:                       strings.ToLower(opts.Name),
    45  		Description:                     opts.Description,
    46  		OriginalURL:                     opts.OriginalURL,
    47  		OriginalServiceType:             opts.GitServiceType,
    48  		IsPrivate:                       opts.IsPrivate,
    49  		IsFsckEnabled:                   !opts.IsMirror,
    50  		CloseIssuesViaCommitInAnyBranch: setting.Repository.DefaultCloseIssuesViaCommitsInAnyBranch,
    51  		Status:                          opts.Status,
    52  		IsEmpty:                         !opts.AutoInit,
    53  	}
    54  
    55  	if err := db.WithTx(ctx, func(ctx context.Context) error {
    56  		repoPath := repo_model.RepoPath(u.Name, repo.Name)
    57  		isExist, err := util.IsExist(repoPath)
    58  		if err != nil {
    59  			log.Error("Unable to check if %s exists. Error: %v", repoPath, err)
    60  			return err
    61  		}
    62  		if !isExist {
    63  			return repo_model.ErrRepoNotExist{
    64  				OwnerName: u.Name,
    65  				Name:      repo.Name,
    66  			}
    67  		}
    68  
    69  		if err := repo_module.CreateRepositoryByExample(ctx, doer, u, repo, true, false); err != nil {
    70  			return err
    71  		}
    72  
    73  		// Re-fetch the repository from database before updating it (else it would
    74  		// override changes that were done earlier with sql)
    75  		if repo, err = repo_model.GetRepositoryByID(ctx, repo.ID); err != nil {
    76  			return fmt.Errorf("getRepositoryByID: %w", err)
    77  		}
    78  
    79  		if err := adoptRepository(ctx, repoPath, repo, opts.DefaultBranch); err != nil {
    80  			return fmt.Errorf("adoptRepository: %w", err)
    81  		}
    82  
    83  		if err := repo_module.CheckDaemonExportOK(ctx, repo); err != nil {
    84  			return fmt.Errorf("checkDaemonExportOK: %w", err)
    85  		}
    86  
    87  		// Initialize Issue Labels if selected
    88  		if len(opts.IssueLabels) > 0 {
    89  			if err := repo_module.InitializeLabels(ctx, repo.ID, opts.IssueLabels, false); err != nil {
    90  				return fmt.Errorf("InitializeLabels: %w", err)
    91  			}
    92  		}
    93  
    94  		if stdout, _, err := git.NewCommand(ctx, "update-server-info").
    95  			SetDescription(fmt.Sprintf("CreateRepository(git update-server-info): %s", repoPath)).
    96  			RunStdString(&git.RunOpts{Dir: repoPath}); err != nil {
    97  			log.Error("CreateRepository(git update-server-info) in %v: Stdout: %s\nError: %v", repo, stdout, err)
    98  			return fmt.Errorf("CreateRepository(git update-server-info): %w", err)
    99  		}
   100  		return nil
   101  	}); err != nil {
   102  		return nil, err
   103  	}
   104  
   105  	notify_service.AdoptRepository(ctx, doer, u, repo)
   106  
   107  	return repo, nil
   108  }
   109  
   110  func adoptRepository(ctx context.Context, repoPath string, repo *repo_model.Repository, defaultBranch string) (err error) {
   111  	isExist, err := util.IsExist(repoPath)
   112  	if err != nil {
   113  		log.Error("Unable to check if %s exists. Error: %v", repoPath, err)
   114  		return err
   115  	}
   116  	if !isExist {
   117  		return fmt.Errorf("adoptRepository: path does not already exist: %s", repoPath)
   118  	}
   119  
   120  	if err := repo_module.CreateDelegateHooks(repoPath); err != nil {
   121  		return fmt.Errorf("createDelegateHooks: %w", err)
   122  	}
   123  
   124  	repo.IsEmpty = false
   125  
   126  	if len(defaultBranch) > 0 {
   127  		repo.DefaultBranch = defaultBranch
   128  
   129  		if err = gitrepo.SetDefaultBranch(ctx, repo, repo.DefaultBranch); err != nil {
   130  			return fmt.Errorf("setDefaultBranch: %w", err)
   131  		}
   132  	} else {
   133  		repo.DefaultBranch, err = gitrepo.GetDefaultBranch(ctx, repo)
   134  		if err != nil {
   135  			repo.DefaultBranch = setting.Repository.DefaultBranch
   136  			if err = gitrepo.SetDefaultBranch(ctx, repo, repo.DefaultBranch); err != nil {
   137  				return fmt.Errorf("setDefaultBranch: %w", err)
   138  			}
   139  		}
   140  	}
   141  
   142  	// Don't bother looking this repo in the context it won't be there
   143  	gitRepo, err := gitrepo.OpenRepository(ctx, repo)
   144  	if err != nil {
   145  		return fmt.Errorf("openRepository: %w", err)
   146  	}
   147  	defer gitRepo.Close()
   148  
   149  	if _, err = repo_module.SyncRepoBranchesWithRepo(ctx, repo, gitRepo, 0); err != nil {
   150  		return fmt.Errorf("SyncRepoBranchesWithRepo: %w", err)
   151  	}
   152  
   153  	if err = repo_module.SyncReleasesWithTags(ctx, repo, gitRepo); err != nil {
   154  		return fmt.Errorf("SyncReleasesWithTags: %w", err)
   155  	}
   156  
   157  	branches, _ := git_model.FindBranchNames(ctx, git_model.FindBranchOptions{
   158  		RepoID:          repo.ID,
   159  		ListOptions:     db.ListOptionsAll,
   160  		IsDeletedBranch: optional.Some(false),
   161  	})
   162  
   163  	found := false
   164  	hasDefault := false
   165  	hasMaster := false
   166  	hasMain := false
   167  	for _, branch := range branches {
   168  		if branch == repo.DefaultBranch {
   169  			found = true
   170  			break
   171  		} else if branch == setting.Repository.DefaultBranch {
   172  			hasDefault = true
   173  		} else if branch == "master" {
   174  			hasMaster = true
   175  		} else if branch == "main" {
   176  			hasMain = true
   177  		}
   178  	}
   179  	if !found {
   180  		if hasDefault {
   181  			repo.DefaultBranch = setting.Repository.DefaultBranch
   182  		} else if hasMaster {
   183  			repo.DefaultBranch = "master"
   184  		} else if hasMain {
   185  			repo.DefaultBranch = "main"
   186  		} else if len(branches) > 0 {
   187  			repo.DefaultBranch = branches[0]
   188  		} else {
   189  			repo.IsEmpty = true
   190  			repo.DefaultBranch = setting.Repository.DefaultBranch
   191  		}
   192  
   193  		if err = gitrepo.SetDefaultBranch(ctx, repo, repo.DefaultBranch); err != nil {
   194  			return fmt.Errorf("setDefaultBranch: %w", err)
   195  		}
   196  	}
   197  	if err = repo_module.UpdateRepository(ctx, repo, false); err != nil {
   198  		return fmt.Errorf("updateRepository: %w", err)
   199  	}
   200  
   201  	return nil
   202  }
   203  
   204  // DeleteUnadoptedRepository deletes unadopted repository files from the filesystem
   205  func DeleteUnadoptedRepository(ctx context.Context, doer, u *user_model.User, repoName string) error {
   206  	if err := repo_model.IsUsableRepoName(repoName); err != nil {
   207  		return err
   208  	}
   209  
   210  	repoPath := repo_model.RepoPath(u.Name, repoName)
   211  	isExist, err := util.IsExist(repoPath)
   212  	if err != nil {
   213  		log.Error("Unable to check if %s exists. Error: %v", repoPath, err)
   214  		return err
   215  	}
   216  	if !isExist {
   217  		return repo_model.ErrRepoNotExist{
   218  			OwnerName: u.Name,
   219  			Name:      repoName,
   220  		}
   221  	}
   222  
   223  	if exist, err := repo_model.IsRepositoryModelExist(ctx, u, repoName); err != nil {
   224  		return err
   225  	} else if exist {
   226  		return repo_model.ErrRepoAlreadyExist{
   227  			Uname: u.Name,
   228  			Name:  repoName,
   229  		}
   230  	}
   231  
   232  	return util.RemoveAll(repoPath)
   233  }
   234  
   235  type unadoptedRepositories struct {
   236  	repositories []string
   237  	index        int
   238  	start        int
   239  	end          int
   240  }
   241  
   242  func (unadopted *unadoptedRepositories) add(repository string) {
   243  	if unadopted.index >= unadopted.start && unadopted.index < unadopted.end {
   244  		unadopted.repositories = append(unadopted.repositories, repository)
   245  	}
   246  	unadopted.index++
   247  }
   248  
   249  func checkUnadoptedRepositories(ctx context.Context, userName string, repoNamesToCheck []string, unadopted *unadoptedRepositories) error {
   250  	if len(repoNamesToCheck) == 0 {
   251  		return nil
   252  	}
   253  	ctxUser, err := user_model.GetUserByName(ctx, userName)
   254  	if err != nil {
   255  		if user_model.IsErrUserNotExist(err) {
   256  			log.Debug("Missing user: %s", userName)
   257  			return nil
   258  		}
   259  		return err
   260  	}
   261  	repos, _, err := repo_model.GetUserRepositories(ctx, &repo_model.SearchRepoOptions{
   262  		Actor:   ctxUser,
   263  		Private: true,
   264  		ListOptions: db.ListOptions{
   265  			Page:     1,
   266  			PageSize: len(repoNamesToCheck),
   267  		}, LowerNames: repoNamesToCheck,
   268  	})
   269  	if err != nil {
   270  		return err
   271  	}
   272  	if len(repos) == len(repoNamesToCheck) {
   273  		return nil
   274  	}
   275  	repoNames := make(container.Set[string], len(repos))
   276  	for _, repo := range repos {
   277  		repoNames.Add(repo.LowerName)
   278  	}
   279  	for _, repoName := range repoNamesToCheck {
   280  		if !repoNames.Contains(repoName) {
   281  			unadopted.add(path.Join(userName, repoName)) // These are not used as filepaths - but as reponames - therefore use path.Join not filepath.Join
   282  		}
   283  	}
   284  	return nil
   285  }
   286  
   287  // ListUnadoptedRepositories lists all the unadopted repositories that match the provided query
   288  func ListUnadoptedRepositories(ctx context.Context, query string, opts *db.ListOptions) ([]string, int, error) {
   289  	globUser, _ := glob.Compile("*")
   290  	globRepo, _ := glob.Compile("*")
   291  
   292  	qsplit := strings.SplitN(query, "/", 2)
   293  	if len(qsplit) > 0 && len(query) > 0 {
   294  		var err error
   295  		globUser, err = glob.Compile(qsplit[0])
   296  		if err != nil {
   297  			log.Info("Invalid glob expression '%s' (skipped): %v", qsplit[0], err)
   298  		}
   299  		if len(qsplit) > 1 {
   300  			globRepo, err = glob.Compile(qsplit[1])
   301  			if err != nil {
   302  				log.Info("Invalid glob expression '%s' (skipped): %v", qsplit[1], err)
   303  			}
   304  		}
   305  	}
   306  	var repoNamesToCheck []string
   307  
   308  	start := (opts.Page - 1) * opts.PageSize
   309  	unadopted := &unadoptedRepositories{
   310  		repositories: make([]string, 0, opts.PageSize),
   311  		start:        start,
   312  		end:          start + opts.PageSize,
   313  		index:        0,
   314  	}
   315  
   316  	var userName string
   317  
   318  	// We're going to iterate by pagesize.
   319  	root := filepath.Clean(setting.RepoRootPath)
   320  	if err := filepath.WalkDir(root, func(path string, d os.DirEntry, err error) error {
   321  		if err != nil {
   322  			return err
   323  		}
   324  		if !d.IsDir() || path == root {
   325  			return nil
   326  		}
   327  
   328  		name := d.Name()
   329  
   330  		if !strings.ContainsRune(path[len(root)+1:], filepath.Separator) {
   331  			// Got a new user
   332  			if err = checkUnadoptedRepositories(ctx, userName, repoNamesToCheck, unadopted); err != nil {
   333  				return err
   334  			}
   335  			repoNamesToCheck = repoNamesToCheck[:0]
   336  
   337  			if !globUser.Match(name) {
   338  				return filepath.SkipDir
   339  			}
   340  
   341  			userName = name
   342  			return nil
   343  		}
   344  
   345  		if !strings.HasSuffix(name, ".git") {
   346  			return filepath.SkipDir
   347  		}
   348  		name = name[:len(name)-4]
   349  		if repo_model.IsUsableRepoName(name) != nil || strings.ToLower(name) != name || !globRepo.Match(name) {
   350  			return filepath.SkipDir
   351  		}
   352  
   353  		repoNamesToCheck = append(repoNamesToCheck, name)
   354  		if len(repoNamesToCheck) >= setting.Database.IterateBufferSize {
   355  			if err = checkUnadoptedRepositories(ctx, userName, repoNamesToCheck, unadopted); err != nil {
   356  				return err
   357  			}
   358  			repoNamesToCheck = repoNamesToCheck[:0]
   359  		}
   360  		return filepath.SkipDir
   361  	}); err != nil {
   362  		return nil, 0, err
   363  	}
   364  
   365  	if err := checkUnadoptedRepositories(ctx, userName, repoNamesToCheck, unadopted); err != nil {
   366  		return nil, 0, err
   367  	}
   368  
   369  	return unadopted.repositories, unadopted.index, nil
   370  }