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

     1  // Copyright 2023 The Gitea Authors. All rights reserved.
     2  // SPDX-License-Identifier: MIT
     3  
     4  package repository
     5  
     6  import (
     7  	"bytes"
     8  	"context"
     9  	"fmt"
    10  	"os"
    11  	"path/filepath"
    12  	"strings"
    13  	"time"
    14  
    15  	"code.gitea.io/gitea/models/db"
    16  	repo_model "code.gitea.io/gitea/models/repo"
    17  	user_model "code.gitea.io/gitea/models/user"
    18  	"code.gitea.io/gitea/modules/git"
    19  	"code.gitea.io/gitea/modules/gitrepo"
    20  	"code.gitea.io/gitea/modules/log"
    21  	"code.gitea.io/gitea/modules/options"
    22  	repo_module "code.gitea.io/gitea/modules/repository"
    23  	"code.gitea.io/gitea/modules/setting"
    24  	api "code.gitea.io/gitea/modules/structs"
    25  	"code.gitea.io/gitea/modules/templates/vars"
    26  	"code.gitea.io/gitea/modules/util"
    27  )
    28  
    29  // CreateRepoOptions contains the create repository options
    30  type CreateRepoOptions struct {
    31  	Name             string
    32  	Description      string
    33  	OriginalURL      string
    34  	GitServiceType   api.GitServiceType
    35  	Gitignores       string
    36  	IssueLabels      string
    37  	License          string
    38  	Readme           string
    39  	DefaultBranch    string
    40  	IsPrivate        bool
    41  	IsMirror         bool
    42  	IsTemplate       bool
    43  	AutoInit         bool
    44  	Status           repo_model.RepositoryStatus
    45  	TrustModel       repo_model.TrustModelType
    46  	MirrorInterval   string
    47  	ObjectFormatName string
    48  }
    49  
    50  func prepareRepoCommit(ctx context.Context, repo *repo_model.Repository, tmpDir, repoPath string, opts CreateRepoOptions) error {
    51  	commitTimeStr := time.Now().Format(time.RFC3339)
    52  	authorSig := repo.Owner.NewGitSig()
    53  
    54  	// Because this may call hooks we should pass in the environment
    55  	env := append(os.Environ(),
    56  		"GIT_AUTHOR_NAME="+authorSig.Name,
    57  		"GIT_AUTHOR_EMAIL="+authorSig.Email,
    58  		"GIT_AUTHOR_DATE="+commitTimeStr,
    59  		"GIT_COMMITTER_NAME="+authorSig.Name,
    60  		"GIT_COMMITTER_EMAIL="+authorSig.Email,
    61  		"GIT_COMMITTER_DATE="+commitTimeStr,
    62  	)
    63  
    64  	// Clone to temporary path and do the init commit.
    65  	if stdout, _, err := git.NewCommand(ctx, "clone").AddDynamicArguments(repoPath, tmpDir).
    66  		SetDescription(fmt.Sprintf("prepareRepoCommit (git clone): %s to %s", repoPath, tmpDir)).
    67  		RunStdString(&git.RunOpts{Dir: "", Env: env}); err != nil {
    68  		log.Error("Failed to clone from %v into %s: stdout: %s\nError: %v", repo, tmpDir, stdout, err)
    69  		return fmt.Errorf("git clone: %w", err)
    70  	}
    71  
    72  	// README
    73  	data, err := options.Readme(opts.Readme)
    74  	if err != nil {
    75  		return fmt.Errorf("GetRepoInitFile[%s]: %w", opts.Readme, err)
    76  	}
    77  
    78  	cloneLink := repo.CloneLink()
    79  	match := map[string]string{
    80  		"Name":           repo.Name,
    81  		"Description":    repo.Description,
    82  		"CloneURL.SSH":   cloneLink.SSH,
    83  		"CloneURL.HTTPS": cloneLink.HTTPS,
    84  		"OwnerName":      repo.OwnerName,
    85  	}
    86  	res, err := vars.Expand(string(data), match)
    87  	if err != nil {
    88  		// here we could just log the error and continue the rendering
    89  		log.Error("unable to expand template vars for repo README: %s, err: %v", opts.Readme, err)
    90  	}
    91  	if err = os.WriteFile(filepath.Join(tmpDir, "README.md"),
    92  		[]byte(res), 0o644); err != nil {
    93  		return fmt.Errorf("write README.md: %w", err)
    94  	}
    95  
    96  	// .gitignore
    97  	if len(opts.Gitignores) > 0 {
    98  		var buf bytes.Buffer
    99  		names := strings.Split(opts.Gitignores, ",")
   100  		for _, name := range names {
   101  			data, err = options.Gitignore(name)
   102  			if err != nil {
   103  				return fmt.Errorf("GetRepoInitFile[%s]: %w", name, err)
   104  			}
   105  			buf.WriteString("# ---> " + name + "\n")
   106  			buf.Write(data)
   107  			buf.WriteString("\n")
   108  		}
   109  
   110  		if buf.Len() > 0 {
   111  			if err = os.WriteFile(filepath.Join(tmpDir, ".gitignore"), buf.Bytes(), 0o644); err != nil {
   112  				return fmt.Errorf("write .gitignore: %w", err)
   113  			}
   114  		}
   115  	}
   116  
   117  	// LICENSE
   118  	if len(opts.License) > 0 {
   119  		data, err = repo_module.GetLicense(opts.License, &repo_module.LicenseValues{
   120  			Owner: repo.OwnerName,
   121  			Email: authorSig.Email,
   122  			Repo:  repo.Name,
   123  			Year:  time.Now().Format("2006"),
   124  		})
   125  		if err != nil {
   126  			return fmt.Errorf("getLicense[%s]: %w", opts.License, err)
   127  		}
   128  
   129  		if err = os.WriteFile(filepath.Join(tmpDir, "LICENSE"), data, 0o644); err != nil {
   130  			return fmt.Errorf("write LICENSE: %w", err)
   131  		}
   132  	}
   133  
   134  	return nil
   135  }
   136  
   137  // InitRepository initializes README and .gitignore if needed.
   138  func initRepository(ctx context.Context, repoPath string, u *user_model.User, repo *repo_model.Repository, opts CreateRepoOptions) (err error) {
   139  	if err = repo_module.CheckInitRepository(ctx, repo.OwnerName, repo.Name, opts.ObjectFormatName); err != nil {
   140  		return err
   141  	}
   142  
   143  	// Initialize repository according to user's choice.
   144  	if opts.AutoInit {
   145  		tmpDir, err := os.MkdirTemp(os.TempDir(), "gitea-"+repo.Name)
   146  		if err != nil {
   147  			return fmt.Errorf("Failed to create temp dir for repository %s: %w", repo.RepoPath(), err)
   148  		}
   149  		defer func() {
   150  			if err := util.RemoveAll(tmpDir); err != nil {
   151  				log.Warn("Unable to remove temporary directory: %s: Error: %v", tmpDir, err)
   152  			}
   153  		}()
   154  
   155  		if err = prepareRepoCommit(ctx, repo, tmpDir, repoPath, opts); err != nil {
   156  			return fmt.Errorf("prepareRepoCommit: %w", err)
   157  		}
   158  
   159  		// Apply changes and commit.
   160  		if err = initRepoCommit(ctx, tmpDir, repo, u, opts.DefaultBranch); err != nil {
   161  			return fmt.Errorf("initRepoCommit: %w", err)
   162  		}
   163  	}
   164  
   165  	// Re-fetch the repository from database before updating it (else it would
   166  	// override changes that were done earlier with sql)
   167  	if repo, err = repo_model.GetRepositoryByID(ctx, repo.ID); err != nil {
   168  		return fmt.Errorf("getRepositoryByID: %w", err)
   169  	}
   170  
   171  	if !opts.AutoInit {
   172  		repo.IsEmpty = true
   173  	}
   174  
   175  	repo.DefaultBranch = setting.Repository.DefaultBranch
   176  	repo.DefaultWikiBranch = setting.Repository.DefaultBranch
   177  
   178  	if len(opts.DefaultBranch) > 0 {
   179  		repo.DefaultBranch = opts.DefaultBranch
   180  		if err = gitrepo.SetDefaultBranch(ctx, repo, repo.DefaultBranch); err != nil {
   181  			return fmt.Errorf("setDefaultBranch: %w", err)
   182  		}
   183  
   184  		if !repo.IsEmpty {
   185  			if _, err := repo_module.SyncRepoBranches(ctx, repo.ID, u.ID); err != nil {
   186  				return fmt.Errorf("SyncRepoBranches: %w", err)
   187  			}
   188  		}
   189  	}
   190  
   191  	if err = UpdateRepository(ctx, repo, false); err != nil {
   192  		return fmt.Errorf("updateRepository: %w", err)
   193  	}
   194  
   195  	return nil
   196  }
   197  
   198  // CreateRepositoryDirectly creates a repository for the user/organization.
   199  func CreateRepositoryDirectly(ctx context.Context, doer, u *user_model.User, opts CreateRepoOptions) (*repo_model.Repository, error) {
   200  	if !doer.IsAdmin && !u.CanCreateRepo() {
   201  		return nil, repo_model.ErrReachLimitOfRepo{
   202  			Limit: u.MaxRepoCreation,
   203  		}
   204  	}
   205  
   206  	if len(opts.DefaultBranch) == 0 {
   207  		opts.DefaultBranch = setting.Repository.DefaultBranch
   208  	}
   209  
   210  	// Check if label template exist
   211  	if len(opts.IssueLabels) > 0 {
   212  		if _, err := repo_module.LoadTemplateLabelsByDisplayName(opts.IssueLabels); err != nil {
   213  			return nil, err
   214  		}
   215  	}
   216  
   217  	if opts.ObjectFormatName == "" {
   218  		opts.ObjectFormatName = git.Sha1ObjectFormat.Name()
   219  	}
   220  
   221  	repo := &repo_model.Repository{
   222  		OwnerID:                         u.ID,
   223  		Owner:                           u,
   224  		OwnerName:                       u.Name,
   225  		Name:                            opts.Name,
   226  		LowerName:                       strings.ToLower(opts.Name),
   227  		Description:                     opts.Description,
   228  		OriginalURL:                     opts.OriginalURL,
   229  		OriginalServiceType:             opts.GitServiceType,
   230  		IsPrivate:                       opts.IsPrivate,
   231  		IsFsckEnabled:                   !opts.IsMirror,
   232  		IsTemplate:                      opts.IsTemplate,
   233  		CloseIssuesViaCommitInAnyBranch: setting.Repository.DefaultCloseIssuesViaCommitsInAnyBranch,
   234  		Status:                          opts.Status,
   235  		IsEmpty:                         !opts.AutoInit,
   236  		TrustModel:                      opts.TrustModel,
   237  		IsMirror:                        opts.IsMirror,
   238  		DefaultBranch:                   opts.DefaultBranch,
   239  		DefaultWikiBranch:               setting.Repository.DefaultBranch,
   240  		ObjectFormatName:                opts.ObjectFormatName,
   241  	}
   242  
   243  	var rollbackRepo *repo_model.Repository
   244  
   245  	if err := db.WithTx(ctx, func(ctx context.Context) error {
   246  		if err := repo_module.CreateRepositoryByExample(ctx, doer, u, repo, false, false); err != nil {
   247  			return err
   248  		}
   249  
   250  		// No need for init mirror.
   251  		if opts.IsMirror {
   252  			return nil
   253  		}
   254  
   255  		repoPath := repo_model.RepoPath(u.Name, repo.Name)
   256  		isExist, err := util.IsExist(repoPath)
   257  		if err != nil {
   258  			log.Error("Unable to check if %s exists. Error: %v", repoPath, err)
   259  			return err
   260  		}
   261  		if isExist {
   262  			// repo already exists - We have two or three options.
   263  			// 1. We fail stating that the directory exists
   264  			// 2. We create the db repository to go with this data and adopt the git repo
   265  			// 3. We delete it and start afresh
   266  			//
   267  			// Previously Gitea would just delete and start afresh - this was naughty.
   268  			// So we will now fail and delegate to other functionality to adopt or delete
   269  			log.Error("Files already exist in %s and we are not going to adopt or delete.", repoPath)
   270  			return repo_model.ErrRepoFilesAlreadyExist{
   271  				Uname: u.Name,
   272  				Name:  repo.Name,
   273  			}
   274  		}
   275  
   276  		if err = initRepository(ctx, repoPath, doer, repo, opts); err != nil {
   277  			if err2 := util.RemoveAll(repoPath); err2 != nil {
   278  				log.Error("initRepository: %v", err)
   279  				return fmt.Errorf(
   280  					"delete repo directory %s/%s failed(2): %v", u.Name, repo.Name, err2)
   281  			}
   282  			return fmt.Errorf("initRepository: %w", err)
   283  		}
   284  
   285  		// Initialize Issue Labels if selected
   286  		if len(opts.IssueLabels) > 0 {
   287  			if err = repo_module.InitializeLabels(ctx, repo.ID, opts.IssueLabels, false); err != nil {
   288  				rollbackRepo = repo
   289  				rollbackRepo.OwnerID = u.ID
   290  				return fmt.Errorf("InitializeLabels: %w", err)
   291  			}
   292  		}
   293  
   294  		if err := repo_module.CheckDaemonExportOK(ctx, repo); err != nil {
   295  			return fmt.Errorf("checkDaemonExportOK: %w", err)
   296  		}
   297  
   298  		if stdout, _, err := git.NewCommand(ctx, "update-server-info").
   299  			SetDescription(fmt.Sprintf("CreateRepository(git update-server-info): %s", repoPath)).
   300  			RunStdString(&git.RunOpts{Dir: repoPath}); err != nil {
   301  			log.Error("CreateRepository(git update-server-info) in %v: Stdout: %s\nError: %v", repo, stdout, err)
   302  			rollbackRepo = repo
   303  			rollbackRepo.OwnerID = u.ID
   304  			return fmt.Errorf("CreateRepository(git update-server-info): %w", err)
   305  		}
   306  		return nil
   307  	}); err != nil {
   308  		if rollbackRepo != nil {
   309  			if errDelete := DeleteRepositoryDirectly(ctx, doer, rollbackRepo.ID); errDelete != nil {
   310  				log.Error("Rollback deleteRepository: %v", errDelete)
   311  			}
   312  		}
   313  
   314  		return nil, err
   315  	}
   316  
   317  	return repo, nil
   318  }