code.gitea.io/gitea@v1.19.3/modules/repository/create.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  	"os"
    10  	"path"
    11  	"path/filepath"
    12  	"strings"
    13  
    14  	"code.gitea.io/gitea/models"
    15  	activities_model "code.gitea.io/gitea/models/activities"
    16  	"code.gitea.io/gitea/models/db"
    17  	git_model "code.gitea.io/gitea/models/git"
    18  	"code.gitea.io/gitea/models/organization"
    19  	"code.gitea.io/gitea/models/perm"
    20  	access_model "code.gitea.io/gitea/models/perm/access"
    21  	repo_model "code.gitea.io/gitea/models/repo"
    22  	"code.gitea.io/gitea/models/unit"
    23  	user_model "code.gitea.io/gitea/models/user"
    24  	"code.gitea.io/gitea/models/webhook"
    25  	"code.gitea.io/gitea/modules/git"
    26  	"code.gitea.io/gitea/modules/log"
    27  	"code.gitea.io/gitea/modules/setting"
    28  	api "code.gitea.io/gitea/modules/structs"
    29  	"code.gitea.io/gitea/modules/util"
    30  )
    31  
    32  // CreateRepositoryByExample creates a repository for the user/organization.
    33  func CreateRepositoryByExample(ctx context.Context, doer, u *user_model.User, repo *repo_model.Repository, overwriteOrAdopt, isFork bool) (err error) {
    34  	if err = repo_model.IsUsableRepoName(repo.Name); err != nil {
    35  		return err
    36  	}
    37  
    38  	has, err := repo_model.IsRepositoryExist(ctx, u, repo.Name)
    39  	if err != nil {
    40  		return fmt.Errorf("IsRepositoryExist: %w", err)
    41  	} else if has {
    42  		return repo_model.ErrRepoAlreadyExist{
    43  			Uname: u.Name,
    44  			Name:  repo.Name,
    45  		}
    46  	}
    47  
    48  	repoPath := repo_model.RepoPath(u.Name, repo.Name)
    49  	isExist, err := util.IsExist(repoPath)
    50  	if err != nil {
    51  		log.Error("Unable to check if %s exists. Error: %v", repoPath, err)
    52  		return err
    53  	}
    54  	if !overwriteOrAdopt && isExist {
    55  		log.Error("Files already exist in %s and we are not going to adopt or delete.", repoPath)
    56  		return repo_model.ErrRepoFilesAlreadyExist{
    57  			Uname: u.Name,
    58  			Name:  repo.Name,
    59  		}
    60  	}
    61  
    62  	if err = db.Insert(ctx, repo); err != nil {
    63  		return err
    64  	}
    65  	if err = repo_model.DeleteRedirect(ctx, u.ID, repo.Name); err != nil {
    66  		return err
    67  	}
    68  
    69  	// insert units for repo
    70  	defaultUnits := unit.DefaultRepoUnits
    71  	if isFork {
    72  		defaultUnits = unit.DefaultForkRepoUnits
    73  	}
    74  	units := make([]repo_model.RepoUnit, 0, len(defaultUnits))
    75  	for _, tp := range defaultUnits {
    76  		if tp == unit.TypeIssues {
    77  			units = append(units, repo_model.RepoUnit{
    78  				RepoID: repo.ID,
    79  				Type:   tp,
    80  				Config: &repo_model.IssuesConfig{
    81  					EnableTimetracker:                setting.Service.DefaultEnableTimetracking,
    82  					AllowOnlyContributorsToTrackTime: setting.Service.DefaultAllowOnlyContributorsToTrackTime,
    83  					EnableDependencies:               setting.Service.DefaultEnableDependencies,
    84  				},
    85  			})
    86  		} else if tp == unit.TypePullRequests {
    87  			units = append(units, repo_model.RepoUnit{
    88  				RepoID: repo.ID,
    89  				Type:   tp,
    90  				Config: &repo_model.PullRequestsConfig{AllowMerge: true, AllowRebase: true, AllowRebaseMerge: true, AllowSquash: true, DefaultMergeStyle: repo_model.MergeStyle(setting.Repository.PullRequest.DefaultMergeStyle), AllowRebaseUpdate: true},
    91  			})
    92  		} else {
    93  			units = append(units, repo_model.RepoUnit{
    94  				RepoID: repo.ID,
    95  				Type:   tp,
    96  			})
    97  		}
    98  	}
    99  
   100  	if err = db.Insert(ctx, units); err != nil {
   101  		return err
   102  	}
   103  
   104  	// Remember visibility preference.
   105  	u.LastRepoVisibility = repo.IsPrivate
   106  	if err = user_model.UpdateUserCols(ctx, u, "last_repo_visibility"); err != nil {
   107  		return fmt.Errorf("UpdateUserCols: %w", err)
   108  	}
   109  
   110  	if err = user_model.IncrUserRepoNum(ctx, u.ID); err != nil {
   111  		return fmt.Errorf("IncrUserRepoNum: %w", err)
   112  	}
   113  	u.NumRepos++
   114  
   115  	// Give access to all members in teams with access to all repositories.
   116  	if u.IsOrganization() {
   117  		teams, err := organization.FindOrgTeams(ctx, u.ID)
   118  		if err != nil {
   119  			return fmt.Errorf("FindOrgTeams: %w", err)
   120  		}
   121  		for _, t := range teams {
   122  			if t.IncludesAllRepositories {
   123  				if err := models.AddRepository(ctx, t, repo); err != nil {
   124  					return fmt.Errorf("AddRepository: %w", err)
   125  				}
   126  			}
   127  		}
   128  
   129  		if isAdmin, err := access_model.IsUserRepoAdmin(ctx, repo, doer); err != nil {
   130  			return fmt.Errorf("IsUserRepoAdmin: %w", err)
   131  		} else if !isAdmin {
   132  			// Make creator repo admin if it wasn't assigned automatically
   133  			if err = AddCollaborator(ctx, repo, doer); err != nil {
   134  				return fmt.Errorf("AddCollaborator: %w", err)
   135  			}
   136  			if err = repo_model.ChangeCollaborationAccessMode(ctx, repo, doer.ID, perm.AccessModeAdmin); err != nil {
   137  				return fmt.Errorf("ChangeCollaborationAccessModeCtx: %w", err)
   138  			}
   139  		}
   140  	} else if err = access_model.RecalculateAccesses(ctx, repo); err != nil {
   141  		// Organization automatically called this in AddRepository method.
   142  		return fmt.Errorf("RecalculateAccesses: %w", err)
   143  	}
   144  
   145  	if setting.Service.AutoWatchNewRepos {
   146  		if err = repo_model.WatchRepo(ctx, doer.ID, repo.ID, true); err != nil {
   147  			return fmt.Errorf("WatchRepo: %w", err)
   148  		}
   149  	}
   150  
   151  	if err = webhook.CopyDefaultWebhooksToRepo(ctx, repo.ID); err != nil {
   152  		return fmt.Errorf("CopyDefaultWebhooksToRepo: %w", err)
   153  	}
   154  
   155  	return nil
   156  }
   157  
   158  // CreateRepoOptions contains the create repository options
   159  type CreateRepoOptions struct {
   160  	Name           string
   161  	Description    string
   162  	OriginalURL    string
   163  	GitServiceType api.GitServiceType
   164  	Gitignores     string
   165  	IssueLabels    string
   166  	License        string
   167  	Readme         string
   168  	DefaultBranch  string
   169  	IsPrivate      bool
   170  	IsMirror       bool
   171  	IsTemplate     bool
   172  	AutoInit       bool
   173  	Status         repo_model.RepositoryStatus
   174  	TrustModel     repo_model.TrustModelType
   175  	MirrorInterval string
   176  }
   177  
   178  // CreateRepository creates a repository for the user/organization.
   179  func CreateRepository(doer, u *user_model.User, opts CreateRepoOptions) (*repo_model.Repository, error) {
   180  	if !doer.IsAdmin && !u.CanCreateRepo() {
   181  		return nil, repo_model.ErrReachLimitOfRepo{
   182  			Limit: u.MaxRepoCreation,
   183  		}
   184  	}
   185  
   186  	if len(opts.DefaultBranch) == 0 {
   187  		opts.DefaultBranch = setting.Repository.DefaultBranch
   188  	}
   189  
   190  	// Check if label template exist
   191  	if len(opts.IssueLabels) > 0 {
   192  		if _, err := LoadTemplateLabelsByDisplayName(opts.IssueLabels); err != nil {
   193  			return nil, err
   194  		}
   195  	}
   196  
   197  	repo := &repo_model.Repository{
   198  		OwnerID:                         u.ID,
   199  		Owner:                           u,
   200  		OwnerName:                       u.Name,
   201  		Name:                            opts.Name,
   202  		LowerName:                       strings.ToLower(opts.Name),
   203  		Description:                     opts.Description,
   204  		OriginalURL:                     opts.OriginalURL,
   205  		OriginalServiceType:             opts.GitServiceType,
   206  		IsPrivate:                       opts.IsPrivate,
   207  		IsFsckEnabled:                   !opts.IsMirror,
   208  		IsTemplate:                      opts.IsTemplate,
   209  		CloseIssuesViaCommitInAnyBranch: setting.Repository.DefaultCloseIssuesViaCommitsInAnyBranch,
   210  		Status:                          opts.Status,
   211  		IsEmpty:                         !opts.AutoInit,
   212  		TrustModel:                      opts.TrustModel,
   213  		IsMirror:                        opts.IsMirror,
   214  		DefaultBranch:                   opts.DefaultBranch,
   215  	}
   216  
   217  	var rollbackRepo *repo_model.Repository
   218  
   219  	if err := db.WithTx(db.DefaultContext, func(ctx context.Context) error {
   220  		if err := CreateRepositoryByExample(ctx, doer, u, repo, false, false); err != nil {
   221  			return err
   222  		}
   223  
   224  		// No need for init mirror.
   225  		if opts.IsMirror {
   226  			return nil
   227  		}
   228  
   229  		repoPath := repo_model.RepoPath(u.Name, repo.Name)
   230  		isExist, err := util.IsExist(repoPath)
   231  		if err != nil {
   232  			log.Error("Unable to check if %s exists. Error: %v", repoPath, err)
   233  			return err
   234  		}
   235  		if isExist {
   236  			// repo already exists - We have two or three options.
   237  			// 1. We fail stating that the directory exists
   238  			// 2. We create the db repository to go with this data and adopt the git repo
   239  			// 3. We delete it and start afresh
   240  			//
   241  			// Previously Gitea would just delete and start afresh - this was naughty.
   242  			// So we will now fail and delegate to other functionality to adopt or delete
   243  			log.Error("Files already exist in %s and we are not going to adopt or delete.", repoPath)
   244  			return repo_model.ErrRepoFilesAlreadyExist{
   245  				Uname: u.Name,
   246  				Name:  repo.Name,
   247  			}
   248  		}
   249  
   250  		if err = initRepository(ctx, repoPath, doer, repo, opts); err != nil {
   251  			if err2 := util.RemoveAll(repoPath); err2 != nil {
   252  				log.Error("initRepository: %v", err)
   253  				return fmt.Errorf(
   254  					"delete repo directory %s/%s failed(2): %v", u.Name, repo.Name, err2)
   255  			}
   256  			return fmt.Errorf("initRepository: %w", err)
   257  		}
   258  
   259  		// Initialize Issue Labels if selected
   260  		if len(opts.IssueLabels) > 0 {
   261  			if err = InitializeLabels(ctx, repo.ID, opts.IssueLabels, false); err != nil {
   262  				rollbackRepo = repo
   263  				rollbackRepo.OwnerID = u.ID
   264  				return fmt.Errorf("InitializeLabels: %w", err)
   265  			}
   266  		}
   267  
   268  		if err := CheckDaemonExportOK(ctx, repo); err != nil {
   269  			return fmt.Errorf("checkDaemonExportOK: %w", err)
   270  		}
   271  
   272  		if stdout, _, err := git.NewCommand(ctx, "update-server-info").
   273  			SetDescription(fmt.Sprintf("CreateRepository(git update-server-info): %s", repoPath)).
   274  			RunStdString(&git.RunOpts{Dir: repoPath}); err != nil {
   275  			log.Error("CreateRepository(git update-server-info) in %v: Stdout: %s\nError: %v", repo, stdout, err)
   276  			rollbackRepo = repo
   277  			rollbackRepo.OwnerID = u.ID
   278  			return fmt.Errorf("CreateRepository(git update-server-info): %w", err)
   279  		}
   280  		return nil
   281  	}); err != nil {
   282  		if rollbackRepo != nil {
   283  			if errDelete := models.DeleteRepository(doer, rollbackRepo.OwnerID, rollbackRepo.ID); errDelete != nil {
   284  				log.Error("Rollback deleteRepository: %v", errDelete)
   285  			}
   286  		}
   287  
   288  		return nil, err
   289  	}
   290  
   291  	return repo, nil
   292  }
   293  
   294  const notRegularFileMode = os.ModeSymlink | os.ModeNamedPipe | os.ModeSocket | os.ModeDevice | os.ModeCharDevice | os.ModeIrregular
   295  
   296  // getDirectorySize returns the disk consumption for a given path
   297  func getDirectorySize(path string) (int64, error) {
   298  	var size int64
   299  	err := filepath.WalkDir(path, func(_ string, info os.DirEntry, err error) error {
   300  		if err != nil {
   301  			if os.IsNotExist(err) { // ignore the error because the file maybe deleted during traversing.
   302  				return nil
   303  			}
   304  			return err
   305  		}
   306  		if info.IsDir() {
   307  			return nil
   308  		}
   309  		f, err := info.Info()
   310  		if err != nil {
   311  			return err
   312  		}
   313  		if (f.Mode() & notRegularFileMode) == 0 {
   314  			size += f.Size()
   315  		}
   316  		return err
   317  	})
   318  	return size, err
   319  }
   320  
   321  // UpdateRepoSize updates the repository size, calculating it using getDirectorySize
   322  func UpdateRepoSize(ctx context.Context, repo *repo_model.Repository) error {
   323  	size, err := getDirectorySize(repo.RepoPath())
   324  	if err != nil {
   325  		return fmt.Errorf("updateSize: %w", err)
   326  	}
   327  
   328  	lfsSize, err := git_model.GetRepoLFSSize(ctx, repo.ID)
   329  	if err != nil {
   330  		return fmt.Errorf("updateSize: GetLFSMetaObjects: %w", err)
   331  	}
   332  
   333  	return repo_model.UpdateRepoSize(ctx, repo.ID, size+lfsSize)
   334  }
   335  
   336  // CheckDaemonExportOK creates/removes git-daemon-export-ok for git-daemon...
   337  func CheckDaemonExportOK(ctx context.Context, repo *repo_model.Repository) error {
   338  	if err := repo.LoadOwner(ctx); err != nil {
   339  		return err
   340  	}
   341  
   342  	// Create/Remove git-daemon-export-ok for git-daemon...
   343  	daemonExportFile := path.Join(repo.RepoPath(), `git-daemon-export-ok`)
   344  
   345  	isExist, err := util.IsExist(daemonExportFile)
   346  	if err != nil {
   347  		log.Error("Unable to check if %s exists. Error: %v", daemonExportFile, err)
   348  		return err
   349  	}
   350  
   351  	isPublic := !repo.IsPrivate && repo.Owner.Visibility == api.VisibleTypePublic
   352  	if !isPublic && isExist {
   353  		if err = util.Remove(daemonExportFile); err != nil {
   354  			log.Error("Failed to remove %s: %v", daemonExportFile, err)
   355  		}
   356  	} else if isPublic && !isExist {
   357  		if f, err := os.Create(daemonExportFile); err != nil {
   358  			log.Error("Failed to create %s: %v", daemonExportFile, err)
   359  		} else {
   360  			f.Close()
   361  		}
   362  	}
   363  
   364  	return nil
   365  }
   366  
   367  // UpdateRepository updates a repository with db context
   368  func UpdateRepository(ctx context.Context, repo *repo_model.Repository, visibilityChanged bool) (err error) {
   369  	repo.LowerName = strings.ToLower(repo.Name)
   370  
   371  	e := db.GetEngine(ctx)
   372  
   373  	if _, err = e.ID(repo.ID).AllCols().Update(repo); err != nil {
   374  		return fmt.Errorf("update: %w", err)
   375  	}
   376  
   377  	if err = UpdateRepoSize(ctx, repo); err != nil {
   378  		log.Error("Failed to update size for repository: %v", err)
   379  	}
   380  
   381  	if visibilityChanged {
   382  		if err = repo.LoadOwner(ctx); err != nil {
   383  			return fmt.Errorf("LoadOwner: %w", err)
   384  		}
   385  		if repo.Owner.IsOrganization() {
   386  			// Organization repository need to recalculate access table when visibility is changed.
   387  			if err = access_model.RecalculateTeamAccesses(ctx, repo, 0); err != nil {
   388  				return fmt.Errorf("recalculateTeamAccesses: %w", err)
   389  			}
   390  		}
   391  
   392  		// If repo has become private, we need to set its actions to private.
   393  		if repo.IsPrivate {
   394  			_, err = e.Where("repo_id = ?", repo.ID).Cols("is_private").Update(&activities_model.Action{
   395  				IsPrivate: true,
   396  			})
   397  			if err != nil {
   398  				return err
   399  			}
   400  		}
   401  
   402  		// Create/Remove git-daemon-export-ok for git-daemon...
   403  		if err := CheckDaemonExportOK(ctx, repo); err != nil {
   404  			return err
   405  		}
   406  
   407  		forkRepos, err := repo_model.GetRepositoriesByForkID(ctx, repo.ID)
   408  		if err != nil {
   409  			return fmt.Errorf("getRepositoriesByForkID: %w", err)
   410  		}
   411  		for i := range forkRepos {
   412  			forkRepos[i].IsPrivate = repo.IsPrivate || repo.Owner.Visibility == api.VisibleTypePrivate
   413  			if err = UpdateRepository(ctx, forkRepos[i], true); err != nil {
   414  				return fmt.Errorf("updateRepository[%d]: %w", forkRepos[i].ID, err)
   415  			}
   416  		}
   417  	}
   418  
   419  	return nil
   420  }