code.gitea.io/gitea@v1.19.3/modules/repository/init.go (about)

     1  // Copyright 2019 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  	"sort"
    13  	"strings"
    14  	"time"
    15  
    16  	issues_model "code.gitea.io/gitea/models/issues"
    17  	repo_model "code.gitea.io/gitea/models/repo"
    18  	user_model "code.gitea.io/gitea/models/user"
    19  	"code.gitea.io/gitea/modules/git"
    20  	"code.gitea.io/gitea/modules/label"
    21  	"code.gitea.io/gitea/modules/log"
    22  	"code.gitea.io/gitea/modules/options"
    23  	"code.gitea.io/gitea/modules/setting"
    24  	"code.gitea.io/gitea/modules/templates/vars"
    25  	"code.gitea.io/gitea/modules/util"
    26  	asymkey_service "code.gitea.io/gitea/services/asymkey"
    27  )
    28  
    29  type OptionFile struct {
    30  	DisplayName string
    31  	Description string
    32  }
    33  
    34  var (
    35  	// Gitignores contains the gitiginore files
    36  	Gitignores []string
    37  
    38  	// Licenses contains the license files
    39  	Licenses []string
    40  
    41  	// Readmes contains the readme files
    42  	Readmes []string
    43  
    44  	// LabelTemplateFiles contains the label template files, each item has its DisplayName and Description
    45  	LabelTemplateFiles   []OptionFile
    46  	labelTemplateFileMap = map[string]string{} // DisplayName => FileName mapping
    47  )
    48  
    49  type optionFileList struct {
    50  	all    []string // all files provided by bindata & custom-path. Sorted.
    51  	custom []string // custom files provided by custom-path. Non-sorted, internal use only.
    52  }
    53  
    54  // mergeCustomLabelFiles merges the custom label files. Always use the file's main name (DisplayName) as the key to de-duplicate.
    55  func mergeCustomLabelFiles(fl optionFileList) []string {
    56  	exts := map[string]int{"": 0, ".yml": 1, ".yaml": 2} // "yaml" file has the highest priority to be used.
    57  
    58  	m := map[string]string{}
    59  	merge := func(list []string) {
    60  		sort.Slice(list, func(i, j int) bool { return exts[filepath.Ext(list[i])] < exts[filepath.Ext(list[j])] })
    61  		for _, f := range list {
    62  			m[strings.TrimSuffix(f, filepath.Ext(f))] = f
    63  		}
    64  	}
    65  	merge(fl.all)
    66  	merge(fl.custom)
    67  
    68  	files := make([]string, 0, len(m))
    69  	for _, f := range m {
    70  		files = append(files, f)
    71  	}
    72  	sort.Strings(files)
    73  	return files
    74  }
    75  
    76  // LoadRepoConfig loads the repository config
    77  func LoadRepoConfig() error {
    78  	types := []string{"gitignore", "license", "readme", "label"} // option file directories
    79  	typeFiles := make([]optionFileList, len(types))
    80  	for i, t := range types {
    81  		var err error
    82  		if typeFiles[i].all, err = options.Dir(t); err != nil {
    83  			return fmt.Errorf("failed to list %s files: %w", t, err)
    84  		}
    85  		sort.Strings(typeFiles[i].all)
    86  		customPath := filepath.Join(setting.CustomPath, "options", t)
    87  		if isDir, err := util.IsDir(customPath); err != nil {
    88  			return fmt.Errorf("failed to check custom %s dir: %w", t, err)
    89  		} else if isDir {
    90  			if typeFiles[i].custom, err = util.StatDir(customPath); err != nil {
    91  				return fmt.Errorf("failed to list custom %s files: %w", t, err)
    92  			}
    93  		}
    94  	}
    95  
    96  	Gitignores = typeFiles[0].all
    97  	Licenses = typeFiles[1].all
    98  	Readmes = typeFiles[2].all
    99  
   100  	// Load label templates
   101  	LabelTemplateFiles = nil
   102  	labelTemplateFileMap = map[string]string{}
   103  	for _, file := range mergeCustomLabelFiles(typeFiles[3]) {
   104  		description, err := label.LoadTemplateDescription(file)
   105  		if err != nil {
   106  			return fmt.Errorf("failed to load labels: %w", err)
   107  		}
   108  		displayName := strings.TrimSuffix(file, filepath.Ext(file))
   109  		labelTemplateFileMap[displayName] = file
   110  		LabelTemplateFiles = append(LabelTemplateFiles, OptionFile{DisplayName: displayName, Description: description})
   111  	}
   112  
   113  	// Filter out invalid names and promote preferred licenses.
   114  	sortedLicenses := make([]string, 0, len(Licenses))
   115  	for _, name := range setting.Repository.PreferredLicenses {
   116  		if util.SliceContainsString(Licenses, name, true) {
   117  			sortedLicenses = append(sortedLicenses, name)
   118  		}
   119  	}
   120  	for _, name := range Licenses {
   121  		if !util.SliceContainsString(setting.Repository.PreferredLicenses, name, true) {
   122  			sortedLicenses = append(sortedLicenses, name)
   123  		}
   124  	}
   125  	Licenses = sortedLicenses
   126  	return nil
   127  }
   128  
   129  func prepareRepoCommit(ctx context.Context, repo *repo_model.Repository, tmpDir, repoPath string, opts CreateRepoOptions) error {
   130  	commitTimeStr := time.Now().Format(time.RFC3339)
   131  	authorSig := repo.Owner.NewGitSig()
   132  
   133  	// Because this may call hooks we should pass in the environment
   134  	env := append(os.Environ(),
   135  		"GIT_AUTHOR_NAME="+authorSig.Name,
   136  		"GIT_AUTHOR_EMAIL="+authorSig.Email,
   137  		"GIT_AUTHOR_DATE="+commitTimeStr,
   138  		"GIT_COMMITTER_NAME="+authorSig.Name,
   139  		"GIT_COMMITTER_EMAIL="+authorSig.Email,
   140  		"GIT_COMMITTER_DATE="+commitTimeStr,
   141  	)
   142  
   143  	// Clone to temporary path and do the init commit.
   144  	if stdout, _, err := git.NewCommand(ctx, "clone").AddDynamicArguments(repoPath, tmpDir).
   145  		SetDescription(fmt.Sprintf("prepareRepoCommit (git clone): %s to %s", repoPath, tmpDir)).
   146  		RunStdString(&git.RunOpts{Dir: "", Env: env}); err != nil {
   147  		log.Error("Failed to clone from %v into %s: stdout: %s\nError: %v", repo, tmpDir, stdout, err)
   148  		return fmt.Errorf("git clone: %w", err)
   149  	}
   150  
   151  	// README
   152  	data, err := options.GetRepoInitFile("readme", opts.Readme)
   153  	if err != nil {
   154  		return fmt.Errorf("GetRepoInitFile[%s]: %w", opts.Readme, err)
   155  	}
   156  
   157  	cloneLink := repo.CloneLink()
   158  	match := map[string]string{
   159  		"Name":           repo.Name,
   160  		"Description":    repo.Description,
   161  		"CloneURL.SSH":   cloneLink.SSH,
   162  		"CloneURL.HTTPS": cloneLink.HTTPS,
   163  		"OwnerName":      repo.OwnerName,
   164  	}
   165  	res, err := vars.Expand(string(data), match)
   166  	if err != nil {
   167  		// here we could just log the error and continue the rendering
   168  		log.Error("unable to expand template vars for repo README: %s, err: %v", opts.Readme, err)
   169  	}
   170  	if err = os.WriteFile(filepath.Join(tmpDir, "README.md"),
   171  		[]byte(res), 0o644); err != nil {
   172  		return fmt.Errorf("write README.md: %w", err)
   173  	}
   174  
   175  	// .gitignore
   176  	if len(opts.Gitignores) > 0 {
   177  		var buf bytes.Buffer
   178  		names := strings.Split(opts.Gitignores, ",")
   179  		for _, name := range names {
   180  			data, err = options.GetRepoInitFile("gitignore", name)
   181  			if err != nil {
   182  				return fmt.Errorf("GetRepoInitFile[%s]: %w", name, err)
   183  			}
   184  			buf.WriteString("# ---> " + name + "\n")
   185  			buf.Write(data)
   186  			buf.WriteString("\n")
   187  		}
   188  
   189  		if buf.Len() > 0 {
   190  			if err = os.WriteFile(filepath.Join(tmpDir, ".gitignore"), buf.Bytes(), 0o644); err != nil {
   191  				return fmt.Errorf("write .gitignore: %w", err)
   192  			}
   193  		}
   194  	}
   195  
   196  	// LICENSE
   197  	if len(opts.License) > 0 {
   198  		data, err = options.GetRepoInitFile("license", opts.License)
   199  		if err != nil {
   200  			return fmt.Errorf("GetRepoInitFile[%s]: %w", opts.License, err)
   201  		}
   202  
   203  		if err = os.WriteFile(filepath.Join(tmpDir, "LICENSE"), data, 0o644); err != nil {
   204  			return fmt.Errorf("write LICENSE: %w", err)
   205  		}
   206  	}
   207  
   208  	return nil
   209  }
   210  
   211  // initRepoCommit temporarily changes with work directory.
   212  func initRepoCommit(ctx context.Context, tmpPath string, repo *repo_model.Repository, u *user_model.User, defaultBranch string) (err error) {
   213  	commitTimeStr := time.Now().Format(time.RFC3339)
   214  
   215  	sig := u.NewGitSig()
   216  	// Because this may call hooks we should pass in the environment
   217  	env := append(os.Environ(),
   218  		"GIT_AUTHOR_NAME="+sig.Name,
   219  		"GIT_AUTHOR_EMAIL="+sig.Email,
   220  		"GIT_AUTHOR_DATE="+commitTimeStr,
   221  		"GIT_COMMITTER_DATE="+commitTimeStr,
   222  	)
   223  	committerName := sig.Name
   224  	committerEmail := sig.Email
   225  
   226  	if stdout, _, err := git.NewCommand(ctx, "add", "--all").
   227  		SetDescription(fmt.Sprintf("initRepoCommit (git add): %s", tmpPath)).
   228  		RunStdString(&git.RunOpts{Dir: tmpPath}); err != nil {
   229  		log.Error("git add --all failed: Stdout: %s\nError: %v", stdout, err)
   230  		return fmt.Errorf("git add --all: %w", err)
   231  	}
   232  
   233  	cmd := git.NewCommand(ctx, "commit", "--message=Initial commit").
   234  		AddOptionFormat("--author='%s <%s>'", sig.Name, sig.Email)
   235  
   236  	sign, keyID, signer, _ := asymkey_service.SignInitialCommit(ctx, tmpPath, u)
   237  	if sign {
   238  		cmd.AddOptionFormat("-S%s", keyID)
   239  
   240  		if repo.GetTrustModel() == repo_model.CommitterTrustModel || repo.GetTrustModel() == repo_model.CollaboratorCommitterTrustModel {
   241  			// need to set the committer to the KeyID owner
   242  			committerName = signer.Name
   243  			committerEmail = signer.Email
   244  		}
   245  	} else {
   246  		cmd.AddArguments("--no-gpg-sign")
   247  	}
   248  
   249  	env = append(env,
   250  		"GIT_COMMITTER_NAME="+committerName,
   251  		"GIT_COMMITTER_EMAIL="+committerEmail,
   252  	)
   253  
   254  	if stdout, _, err := cmd.
   255  		SetDescription(fmt.Sprintf("initRepoCommit (git commit): %s", tmpPath)).
   256  		RunStdString(&git.RunOpts{Dir: tmpPath, Env: env}); err != nil {
   257  		log.Error("Failed to commit: %v: Stdout: %s\nError: %v", cmd.String(), stdout, err)
   258  		return fmt.Errorf("git commit: %w", err)
   259  	}
   260  
   261  	if len(defaultBranch) == 0 {
   262  		defaultBranch = setting.Repository.DefaultBranch
   263  	}
   264  
   265  	if stdout, _, err := git.NewCommand(ctx, "push", "origin").AddDynamicArguments("HEAD:" + defaultBranch).
   266  		SetDescription(fmt.Sprintf("initRepoCommit (git push): %s", tmpPath)).
   267  		RunStdString(&git.RunOpts{Dir: tmpPath, Env: InternalPushingEnvironment(u, repo)}); err != nil {
   268  		log.Error("Failed to push back to HEAD: Stdout: %s\nError: %v", stdout, err)
   269  		return fmt.Errorf("git push: %w", err)
   270  	}
   271  
   272  	return nil
   273  }
   274  
   275  func checkInitRepository(ctx context.Context, owner, name string) (err error) {
   276  	// Somehow the directory could exist.
   277  	repoPath := repo_model.RepoPath(owner, name)
   278  	isExist, err := util.IsExist(repoPath)
   279  	if err != nil {
   280  		log.Error("Unable to check if %s exists. Error: %v", repoPath, err)
   281  		return err
   282  	}
   283  	if isExist {
   284  		return repo_model.ErrRepoFilesAlreadyExist{
   285  			Uname: owner,
   286  			Name:  name,
   287  		}
   288  	}
   289  
   290  	// Init git bare new repository.
   291  	if err = git.InitRepository(ctx, repoPath, true); err != nil {
   292  		return fmt.Errorf("git.InitRepository: %w", err)
   293  	} else if err = createDelegateHooks(repoPath); err != nil {
   294  		return fmt.Errorf("createDelegateHooks: %w", err)
   295  	}
   296  	return nil
   297  }
   298  
   299  // InitRepository initializes README and .gitignore if needed.
   300  func initRepository(ctx context.Context, repoPath string, u *user_model.User, repo *repo_model.Repository, opts CreateRepoOptions) (err error) {
   301  	if err = checkInitRepository(ctx, repo.OwnerName, repo.Name); err != nil {
   302  		return err
   303  	}
   304  
   305  	// Initialize repository according to user's choice.
   306  	if opts.AutoInit {
   307  		tmpDir, err := os.MkdirTemp(os.TempDir(), "gitea-"+repo.Name)
   308  		if err != nil {
   309  			return fmt.Errorf("Failed to create temp dir for repository %s: %w", repo.RepoPath(), err)
   310  		}
   311  		defer func() {
   312  			if err := util.RemoveAll(tmpDir); err != nil {
   313  				log.Warn("Unable to remove temporary directory: %s: Error: %v", tmpDir, err)
   314  			}
   315  		}()
   316  
   317  		if err = prepareRepoCommit(ctx, repo, tmpDir, repoPath, opts); err != nil {
   318  			return fmt.Errorf("prepareRepoCommit: %w", err)
   319  		}
   320  
   321  		// Apply changes and commit.
   322  		if err = initRepoCommit(ctx, tmpDir, repo, u, opts.DefaultBranch); err != nil {
   323  			return fmt.Errorf("initRepoCommit: %w", err)
   324  		}
   325  	}
   326  
   327  	// Re-fetch the repository from database before updating it (else it would
   328  	// override changes that were done earlier with sql)
   329  	if repo, err = repo_model.GetRepositoryByID(ctx, repo.ID); err != nil {
   330  		return fmt.Errorf("getRepositoryByID: %w", err)
   331  	}
   332  
   333  	if !opts.AutoInit {
   334  		repo.IsEmpty = true
   335  	}
   336  
   337  	repo.DefaultBranch = setting.Repository.DefaultBranch
   338  
   339  	if len(opts.DefaultBranch) > 0 {
   340  		repo.DefaultBranch = opts.DefaultBranch
   341  		gitRepo, err := git.OpenRepository(ctx, repo.RepoPath())
   342  		if err != nil {
   343  			return fmt.Errorf("openRepository: %w", err)
   344  		}
   345  		defer gitRepo.Close()
   346  		if err = gitRepo.SetDefaultBranch(repo.DefaultBranch); err != nil {
   347  			return fmt.Errorf("setDefaultBranch: %w", err)
   348  		}
   349  	}
   350  
   351  	if err = UpdateRepository(ctx, repo, false); err != nil {
   352  		return fmt.Errorf("updateRepository: %w", err)
   353  	}
   354  
   355  	return nil
   356  }
   357  
   358  // InitializeLabels adds a label set to a repository using a template
   359  func InitializeLabels(ctx context.Context, id int64, labelTemplate string, isOrg bool) error {
   360  	list, err := LoadTemplateLabelsByDisplayName(labelTemplate)
   361  	if err != nil {
   362  		return err
   363  	}
   364  
   365  	labels := make([]*issues_model.Label, len(list))
   366  	for i := 0; i < len(list); i++ {
   367  		labels[i] = &issues_model.Label{
   368  			Name:        list[i].Name,
   369  			Exclusive:   list[i].Exclusive,
   370  			Description: list[i].Description,
   371  			Color:       list[i].Color,
   372  		}
   373  		if isOrg {
   374  			labels[i].OrgID = id
   375  		} else {
   376  			labels[i].RepoID = id
   377  		}
   378  	}
   379  	for _, label := range labels {
   380  		if err = issues_model.NewLabel(ctx, label); err != nil {
   381  			return err
   382  		}
   383  	}
   384  	return nil
   385  }
   386  
   387  // LoadTemplateLabelsByDisplayName loads a label template by its display name
   388  func LoadTemplateLabelsByDisplayName(displayName string) ([]*label.Label, error) {
   389  	if fileName, ok := labelTemplateFileMap[displayName]; ok {
   390  		return label.LoadTemplateFile(fileName)
   391  	}
   392  	return nil, label.ErrTemplateLoad{TemplateFile: displayName, OriginalError: fmt.Errorf("label template %q not found", displayName)}
   393  }