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