mirror of
				https://github.com/go-gitea/gitea.git
				synced 2025-10-31 21:28:11 +09:00 
			
		
		
		
	## Purpose
This is a refactor toward building an abstraction over managing git
repositories.
Afterwards, it does not matter anymore if they are stored on the local
disk or somewhere remote.
## What this PR changes
We used `git.OpenRepository` everywhere previously.
Now, we should split them into two distinct functions:
Firstly, there are temporary repositories which do not change:
```go
git.OpenRepository(ctx, diskPath)
```
Gitea managed repositories having a record in the database in the
`repository` table are moved into the new package `gitrepo`:
```go
gitrepo.OpenRepository(ctx, repo_model.Repo)
```
Why is `repo_model.Repository` the second parameter instead of file
path?
Because then we can easily adapt our repository storage strategy.
The repositories can be stored locally, however, they could just as well
be stored on a remote server.
## Further changes in other PRs
- A Git Command wrapper on package `gitrepo` could be created. i.e.
`NewCommand(ctx, repo_model.Repository, commands...)`. `git.RunOpts{Dir:
repo.RepoPath()}`, the directory should be empty before invoking this
method and it can be filled in the function only. #28940
- Remove the `RepoPath()`/`WikiPath()` functions to reduce the
possibility of mistakes.
---------
Co-authored-by: delvh <dev.lh@web.de>
		
	
		
			
				
	
	
		
			322 lines
		
	
	
		
			10 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			322 lines
		
	
	
		
			10 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
| // Copyright 2023 The Gitea Authors. All rights reserved.
 | |
| // SPDX-License-Identifier: MIT
 | |
| 
 | |
| package repository
 | |
| 
 | |
| import (
 | |
| 	"bytes"
 | |
| 	"context"
 | |
| 	"fmt"
 | |
| 	"os"
 | |
| 	"path/filepath"
 | |
| 	"strings"
 | |
| 	"time"
 | |
| 
 | |
| 	"code.gitea.io/gitea/models/db"
 | |
| 	repo_model "code.gitea.io/gitea/models/repo"
 | |
| 	user_model "code.gitea.io/gitea/models/user"
 | |
| 	"code.gitea.io/gitea/modules/git"
 | |
| 	"code.gitea.io/gitea/modules/gitrepo"
 | |
| 	"code.gitea.io/gitea/modules/log"
 | |
| 	"code.gitea.io/gitea/modules/options"
 | |
| 	repo_module "code.gitea.io/gitea/modules/repository"
 | |
| 	"code.gitea.io/gitea/modules/setting"
 | |
| 	api "code.gitea.io/gitea/modules/structs"
 | |
| 	"code.gitea.io/gitea/modules/templates/vars"
 | |
| 	"code.gitea.io/gitea/modules/util"
 | |
| )
 | |
| 
 | |
| // CreateRepoOptions contains the create repository options
 | |
| type CreateRepoOptions struct {
 | |
| 	Name             string
 | |
| 	Description      string
 | |
| 	OriginalURL      string
 | |
| 	GitServiceType   api.GitServiceType
 | |
| 	Gitignores       string
 | |
| 	IssueLabels      string
 | |
| 	License          string
 | |
| 	Readme           string
 | |
| 	DefaultBranch    string
 | |
| 	IsPrivate        bool
 | |
| 	IsMirror         bool
 | |
| 	IsTemplate       bool
 | |
| 	AutoInit         bool
 | |
| 	Status           repo_model.RepositoryStatus
 | |
| 	TrustModel       repo_model.TrustModelType
 | |
| 	MirrorInterval   string
 | |
| 	ObjectFormatName string
 | |
| }
 | |
| 
 | |
| func prepareRepoCommit(ctx context.Context, repo *repo_model.Repository, tmpDir, repoPath string, opts CreateRepoOptions) error {
 | |
| 	commitTimeStr := time.Now().Format(time.RFC3339)
 | |
| 	authorSig := repo.Owner.NewGitSig()
 | |
| 
 | |
| 	// Because this may call hooks we should pass in the environment
 | |
| 	env := append(os.Environ(),
 | |
| 		"GIT_AUTHOR_NAME="+authorSig.Name,
 | |
| 		"GIT_AUTHOR_EMAIL="+authorSig.Email,
 | |
| 		"GIT_AUTHOR_DATE="+commitTimeStr,
 | |
| 		"GIT_COMMITTER_NAME="+authorSig.Name,
 | |
| 		"GIT_COMMITTER_EMAIL="+authorSig.Email,
 | |
| 		"GIT_COMMITTER_DATE="+commitTimeStr,
 | |
| 	)
 | |
| 
 | |
| 	// Clone to temporary path and do the init commit.
 | |
| 	if stdout, _, err := git.NewCommand(ctx, "clone").AddDynamicArguments(repoPath, tmpDir).
 | |
| 		SetDescription(fmt.Sprintf("prepareRepoCommit (git clone): %s to %s", repoPath, tmpDir)).
 | |
| 		RunStdString(&git.RunOpts{Dir: "", Env: env}); err != nil {
 | |
| 		log.Error("Failed to clone from %v into %s: stdout: %s\nError: %v", repo, tmpDir, stdout, err)
 | |
| 		return fmt.Errorf("git clone: %w", err)
 | |
| 	}
 | |
| 
 | |
| 	// README
 | |
| 	data, err := options.Readme(opts.Readme)
 | |
| 	if err != nil {
 | |
| 		return fmt.Errorf("GetRepoInitFile[%s]: %w", opts.Readme, err)
 | |
| 	}
 | |
| 
 | |
| 	cloneLink := repo.CloneLink()
 | |
| 	match := map[string]string{
 | |
| 		"Name":           repo.Name,
 | |
| 		"Description":    repo.Description,
 | |
| 		"CloneURL.SSH":   cloneLink.SSH,
 | |
| 		"CloneURL.HTTPS": cloneLink.HTTPS,
 | |
| 		"OwnerName":      repo.OwnerName,
 | |
| 	}
 | |
| 	res, err := vars.Expand(string(data), match)
 | |
| 	if err != nil {
 | |
| 		// here we could just log the error and continue the rendering
 | |
| 		log.Error("unable to expand template vars for repo README: %s, err: %v", opts.Readme, err)
 | |
| 	}
 | |
| 	if err = os.WriteFile(filepath.Join(tmpDir, "README.md"),
 | |
| 		[]byte(res), 0o644); err != nil {
 | |
| 		return fmt.Errorf("write README.md: %w", err)
 | |
| 	}
 | |
| 
 | |
| 	// .gitignore
 | |
| 	if len(opts.Gitignores) > 0 {
 | |
| 		var buf bytes.Buffer
 | |
| 		names := strings.Split(opts.Gitignores, ",")
 | |
| 		for _, name := range names {
 | |
| 			data, err = options.Gitignore(name)
 | |
| 			if err != nil {
 | |
| 				return fmt.Errorf("GetRepoInitFile[%s]: %w", name, err)
 | |
| 			}
 | |
| 			buf.WriteString("# ---> " + name + "\n")
 | |
| 			buf.Write(data)
 | |
| 			buf.WriteString("\n")
 | |
| 		}
 | |
| 
 | |
| 		if buf.Len() > 0 {
 | |
| 			if err = os.WriteFile(filepath.Join(tmpDir, ".gitignore"), buf.Bytes(), 0o644); err != nil {
 | |
| 				return fmt.Errorf("write .gitignore: %w", err)
 | |
| 			}
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	// LICENSE
 | |
| 	if len(opts.License) > 0 {
 | |
| 		data, err = repo_module.GetLicense(opts.License, &repo_module.LicenseValues{
 | |
| 			Owner: repo.OwnerName,
 | |
| 			Email: authorSig.Email,
 | |
| 			Repo:  repo.Name,
 | |
| 			Year:  time.Now().Format("2006"),
 | |
| 		})
 | |
| 		if err != nil {
 | |
| 			return fmt.Errorf("getLicense[%s]: %w", opts.License, err)
 | |
| 		}
 | |
| 
 | |
| 		if err = os.WriteFile(filepath.Join(tmpDir, "LICENSE"), data, 0o644); err != nil {
 | |
| 			return fmt.Errorf("write LICENSE: %w", err)
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	return nil
 | |
| }
 | |
| 
 | |
| // InitRepository initializes README and .gitignore if needed.
 | |
| func initRepository(ctx context.Context, repoPath string, u *user_model.User, repo *repo_model.Repository, opts CreateRepoOptions) (err error) {
 | |
| 	if err = repo_module.CheckInitRepository(ctx, repo.OwnerName, repo.Name, opts.ObjectFormatName); err != nil {
 | |
| 		return err
 | |
| 	}
 | |
| 
 | |
| 	// Initialize repository according to user's choice.
 | |
| 	if opts.AutoInit {
 | |
| 		tmpDir, err := os.MkdirTemp(os.TempDir(), "gitea-"+repo.Name)
 | |
| 		if err != nil {
 | |
| 			return fmt.Errorf("Failed to create temp dir for repository %s: %w", repo.RepoPath(), err)
 | |
| 		}
 | |
| 		defer func() {
 | |
| 			if err := util.RemoveAll(tmpDir); err != nil {
 | |
| 				log.Warn("Unable to remove temporary directory: %s: Error: %v", tmpDir, err)
 | |
| 			}
 | |
| 		}()
 | |
| 
 | |
| 		if err = prepareRepoCommit(ctx, repo, tmpDir, repoPath, opts); err != nil {
 | |
| 			return fmt.Errorf("prepareRepoCommit: %w", err)
 | |
| 		}
 | |
| 
 | |
| 		// Apply changes and commit.
 | |
| 		if err = repo_module.InitRepoCommit(ctx, tmpDir, repo, u, opts.DefaultBranch); err != nil {
 | |
| 			return fmt.Errorf("initRepoCommit: %w", err)
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	// Re-fetch the repository from database before updating it (else it would
 | |
| 	// override changes that were done earlier with sql)
 | |
| 	if repo, err = repo_model.GetRepositoryByID(ctx, repo.ID); err != nil {
 | |
| 		return fmt.Errorf("getRepositoryByID: %w", err)
 | |
| 	}
 | |
| 
 | |
| 	if !opts.AutoInit {
 | |
| 		repo.IsEmpty = true
 | |
| 	}
 | |
| 
 | |
| 	repo.DefaultBranch = setting.Repository.DefaultBranch
 | |
| 
 | |
| 	if len(opts.DefaultBranch) > 0 {
 | |
| 		repo.DefaultBranch = opts.DefaultBranch
 | |
| 		gitRepo, err := gitrepo.OpenRepository(ctx, repo)
 | |
| 		if err != nil {
 | |
| 			return fmt.Errorf("openRepository: %w", err)
 | |
| 		}
 | |
| 		defer gitRepo.Close()
 | |
| 		if err = gitRepo.SetDefaultBranch(repo.DefaultBranch); err != nil {
 | |
| 			return fmt.Errorf("setDefaultBranch: %w", err)
 | |
| 		}
 | |
| 
 | |
| 		if !repo.IsEmpty {
 | |
| 			if _, err := repo_module.SyncRepoBranches(ctx, repo.ID, u.ID); err != nil {
 | |
| 				return fmt.Errorf("SyncRepoBranches: %w", err)
 | |
| 			}
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	if err = UpdateRepository(ctx, repo, false); err != nil {
 | |
| 		return fmt.Errorf("updateRepository: %w", err)
 | |
| 	}
 | |
| 
 | |
| 	return nil
 | |
| }
 | |
| 
 | |
| // CreateRepositoryDirectly creates a repository for the user/organization.
 | |
| func CreateRepositoryDirectly(ctx context.Context, doer, u *user_model.User, opts CreateRepoOptions) (*repo_model.Repository, error) {
 | |
| 	if !doer.IsAdmin && !u.CanCreateRepo() {
 | |
| 		return nil, repo_model.ErrReachLimitOfRepo{
 | |
| 			Limit: u.MaxRepoCreation,
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	if len(opts.DefaultBranch) == 0 {
 | |
| 		opts.DefaultBranch = setting.Repository.DefaultBranch
 | |
| 	}
 | |
| 
 | |
| 	// Check if label template exist
 | |
| 	if len(opts.IssueLabels) > 0 {
 | |
| 		if _, err := repo_module.LoadTemplateLabelsByDisplayName(opts.IssueLabels); err != nil {
 | |
| 			return nil, err
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	if opts.ObjectFormatName == "" {
 | |
| 		opts.ObjectFormatName = git.Sha1ObjectFormat.Name()
 | |
| 	}
 | |
| 
 | |
| 	repo := &repo_model.Repository{
 | |
| 		OwnerID:                         u.ID,
 | |
| 		Owner:                           u,
 | |
| 		OwnerName:                       u.Name,
 | |
| 		Name:                            opts.Name,
 | |
| 		LowerName:                       strings.ToLower(opts.Name),
 | |
| 		Description:                     opts.Description,
 | |
| 		OriginalURL:                     opts.OriginalURL,
 | |
| 		OriginalServiceType:             opts.GitServiceType,
 | |
| 		IsPrivate:                       opts.IsPrivate,
 | |
| 		IsFsckEnabled:                   !opts.IsMirror,
 | |
| 		IsTemplate:                      opts.IsTemplate,
 | |
| 		CloseIssuesViaCommitInAnyBranch: setting.Repository.DefaultCloseIssuesViaCommitsInAnyBranch,
 | |
| 		Status:                          opts.Status,
 | |
| 		IsEmpty:                         !opts.AutoInit,
 | |
| 		TrustModel:                      opts.TrustModel,
 | |
| 		IsMirror:                        opts.IsMirror,
 | |
| 		DefaultBranch:                   opts.DefaultBranch,
 | |
| 		ObjectFormatName:                opts.ObjectFormatName,
 | |
| 	}
 | |
| 
 | |
| 	var rollbackRepo *repo_model.Repository
 | |
| 
 | |
| 	if err := db.WithTx(ctx, func(ctx context.Context) error {
 | |
| 		if err := repo_module.CreateRepositoryByExample(ctx, doer, u, repo, false, false); err != nil {
 | |
| 			return err
 | |
| 		}
 | |
| 
 | |
| 		// No need for init mirror.
 | |
| 		if opts.IsMirror {
 | |
| 			return nil
 | |
| 		}
 | |
| 
 | |
| 		repoPath := repo_model.RepoPath(u.Name, repo.Name)
 | |
| 		isExist, err := util.IsExist(repoPath)
 | |
| 		if err != nil {
 | |
| 			log.Error("Unable to check if %s exists. Error: %v", repoPath, err)
 | |
| 			return err
 | |
| 		}
 | |
| 		if isExist {
 | |
| 			// repo already exists - We have two or three options.
 | |
| 			// 1. We fail stating that the directory exists
 | |
| 			// 2. We create the db repository to go with this data and adopt the git repo
 | |
| 			// 3. We delete it and start afresh
 | |
| 			//
 | |
| 			// Previously Gitea would just delete and start afresh - this was naughty.
 | |
| 			// So we will now fail and delegate to other functionality to adopt or delete
 | |
| 			log.Error("Files already exist in %s and we are not going to adopt or delete.", repoPath)
 | |
| 			return repo_model.ErrRepoFilesAlreadyExist{
 | |
| 				Uname: u.Name,
 | |
| 				Name:  repo.Name,
 | |
| 			}
 | |
| 		}
 | |
| 
 | |
| 		if err = initRepository(ctx, repoPath, doer, repo, opts); err != nil {
 | |
| 			if err2 := util.RemoveAll(repoPath); err2 != nil {
 | |
| 				log.Error("initRepository: %v", err)
 | |
| 				return fmt.Errorf(
 | |
| 					"delete repo directory %s/%s failed(2): %v", u.Name, repo.Name, err2)
 | |
| 			}
 | |
| 			return fmt.Errorf("initRepository: %w", err)
 | |
| 		}
 | |
| 
 | |
| 		// Initialize Issue Labels if selected
 | |
| 		if len(opts.IssueLabels) > 0 {
 | |
| 			if err = repo_module.InitializeLabels(ctx, repo.ID, opts.IssueLabels, false); err != nil {
 | |
| 				rollbackRepo = repo
 | |
| 				rollbackRepo.OwnerID = u.ID
 | |
| 				return fmt.Errorf("InitializeLabels: %w", err)
 | |
| 			}
 | |
| 		}
 | |
| 
 | |
| 		if err := repo_module.CheckDaemonExportOK(ctx, repo); err != nil {
 | |
| 			return fmt.Errorf("checkDaemonExportOK: %w", err)
 | |
| 		}
 | |
| 
 | |
| 		if stdout, _, err := git.NewCommand(ctx, "update-server-info").
 | |
| 			SetDescription(fmt.Sprintf("CreateRepository(git update-server-info): %s", repoPath)).
 | |
| 			RunStdString(&git.RunOpts{Dir: repoPath}); err != nil {
 | |
| 			log.Error("CreateRepository(git update-server-info) in %v: Stdout: %s\nError: %v", repo, stdout, err)
 | |
| 			rollbackRepo = repo
 | |
| 			rollbackRepo.OwnerID = u.ID
 | |
| 			return fmt.Errorf("CreateRepository(git update-server-info): %w", err)
 | |
| 		}
 | |
| 		return nil
 | |
| 	}); err != nil {
 | |
| 		if rollbackRepo != nil {
 | |
| 			if errDelete := DeleteRepositoryDirectly(ctx, doer, rollbackRepo.ID); errDelete != nil {
 | |
| 				log.Error("Rollback deleteRepository: %v", errDelete)
 | |
| 			}
 | |
| 		}
 | |
| 
 | |
| 		return nil, err
 | |
| 	}
 | |
| 
 | |
| 	return repo, nil
 | |
| }
 |