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