forgejo/modules/repository/init.go
Giteabot 5d5f907e7f
Add loading yaml label template files (#22976) (#23232)
Backport #22976

Extract from #11669 and enhancement to #22585 to support exclusive
scoped labels in label templates

* Move label template functionality to label module
* Fix handling of color codes
* Add Advanced label template

Co-authored-by: Lauris BH <lauris@nix.lv>
2023-03-01 21:57:34 -05:00

372 lines
11 KiB
Go

// Copyright 2019 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package repository
import (
"bytes"
"context"
"fmt"
"os"
"path"
"path/filepath"
"sort"
"strings"
"time"
issues_model "code.gitea.io/gitea/models/issues"
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/label"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/options"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/templates/vars"
"code.gitea.io/gitea/modules/util"
asymkey_service "code.gitea.io/gitea/services/asymkey"
)
var (
// Gitignores contains the gitiginore files
Gitignores []string
// Licenses contains the license files
Licenses []string
// Readmes contains the readme files
Readmes []string
// LabelTemplates contains the label template files and the list of labels for each file
LabelTemplates map[string]string
)
// LoadRepoConfig loads the repository config
func LoadRepoConfig() {
// Load .gitignore and license files and readme templates.
types := []string{"gitignore", "license", "readme", "label"}
typeFiles := make([][]string, 4)
for i, t := range types {
files, err := options.Dir(t)
if err != nil {
log.Fatal("Failed to get %s files: %v", t, err)
}
if t == "label" {
for i, f := range files {
ext := strings.ToLower(filepath.Ext(f))
if ext == ".yaml" || ext == ".yml" {
files[i] = f[:len(f)-len(ext)]
}
}
}
customPath := path.Join(setting.CustomPath, "options", t)
isDir, err := util.IsDir(customPath)
if err != nil {
log.Fatal("Failed to get custom %s files: %v", t, err)
}
if isDir {
customFiles, err := util.StatDir(customPath)
if err != nil {
log.Fatal("Failed to get custom %s files: %v", t, err)
}
for _, f := range customFiles {
if !util.SliceContainsString(files, f, true) {
files = append(files, f)
}
}
}
typeFiles[i] = files
}
Gitignores = typeFiles[0]
Licenses = typeFiles[1]
Readmes = typeFiles[2]
LabelTemplatesFiles := typeFiles[3]
sort.Strings(Gitignores)
sort.Strings(Licenses)
sort.Strings(Readmes)
sort.Strings(LabelTemplatesFiles)
// Load label templates
LabelTemplates = make(map[string]string)
for _, templateFile := range LabelTemplatesFiles {
labels, err := label.LoadFormatted(templateFile)
if err != nil {
log.Error("Failed to load labels: %v", err)
}
LabelTemplates[templateFile] = labels
}
// Filter out invalid names and promote preferred licenses.
sortedLicenses := make([]string, 0, len(Licenses))
for _, name := range setting.Repository.PreferredLicenses {
if util.SliceContainsString(Licenses, name, true) {
sortedLicenses = append(sortedLicenses, name)
}
}
for _, name := range Licenses {
if !util.SliceContainsString(setting.Repository.PreferredLicenses, name, true) {
sortedLicenses = append(sortedLicenses, name)
}
}
Licenses = sortedLicenses
}
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.GetRepoInitFile("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.GetRepoInitFile("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 = options.GetRepoInitFile("license", opts.License)
if err != nil {
return fmt.Errorf("GetRepoInitFile[%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
}
// initRepoCommit temporarily changes with work directory.
func initRepoCommit(ctx context.Context, tmpPath string, repo *repo_model.Repository, u *user_model.User, defaultBranch string) (err error) {
commitTimeStr := time.Now().Format(time.RFC3339)
sig := u.NewGitSig()
// Because this may call hooks we should pass in the environment
env := append(os.Environ(),
"GIT_AUTHOR_NAME="+sig.Name,
"GIT_AUTHOR_EMAIL="+sig.Email,
"GIT_AUTHOR_DATE="+commitTimeStr,
"GIT_COMMITTER_DATE="+commitTimeStr,
)
committerName := sig.Name
committerEmail := sig.Email
if stdout, _, err := git.NewCommand(ctx, "add", "--all").
SetDescription(fmt.Sprintf("initRepoCommit (git add): %s", tmpPath)).
RunStdString(&git.RunOpts{Dir: tmpPath}); err != nil {
log.Error("git add --all failed: Stdout: %s\nError: %v", stdout, err)
return fmt.Errorf("git add --all: %w", err)
}
cmd := git.NewCommand(ctx, "commit", "--message=Initial commit").
AddOptionFormat("--author='%s <%s>'", sig.Name, sig.Email)
sign, keyID, signer, _ := asymkey_service.SignInitialCommit(ctx, tmpPath, u)
if sign {
cmd.AddOptionFormat("-S%s", keyID)
if repo.GetTrustModel() == repo_model.CommitterTrustModel || repo.GetTrustModel() == repo_model.CollaboratorCommitterTrustModel {
// need to set the committer to the KeyID owner
committerName = signer.Name
committerEmail = signer.Email
}
} else {
cmd.AddArguments("--no-gpg-sign")
}
env = append(env,
"GIT_COMMITTER_NAME="+committerName,
"GIT_COMMITTER_EMAIL="+committerEmail,
)
if stdout, _, err := cmd.
SetDescription(fmt.Sprintf("initRepoCommit (git commit): %s", tmpPath)).
RunStdString(&git.RunOpts{Dir: tmpPath, Env: env}); err != nil {
log.Error("Failed to commit: %v: Stdout: %s\nError: %v", cmd.String(), stdout, err)
return fmt.Errorf("git commit: %w", err)
}
if len(defaultBranch) == 0 {
defaultBranch = setting.Repository.DefaultBranch
}
if stdout, _, err := git.NewCommand(ctx, "push", "origin").AddDynamicArguments("HEAD:" + defaultBranch).
SetDescription(fmt.Sprintf("initRepoCommit (git push): %s", tmpPath)).
RunStdString(&git.RunOpts{Dir: tmpPath, Env: InternalPushingEnvironment(u, repo)}); err != nil {
log.Error("Failed to push back to HEAD: Stdout: %s\nError: %v", stdout, err)
return fmt.Errorf("git push: %w", err)
}
return nil
}
func checkInitRepository(ctx context.Context, owner, name string) (err error) {
// Somehow the directory could exist.
repoPath := repo_model.RepoPath(owner, 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 {
return repo_model.ErrRepoFilesAlreadyExist{
Uname: owner,
Name: name,
}
}
// Init git bare new repository.
if err = git.InitRepository(ctx, repoPath, true); err != nil {
return fmt.Errorf("git.InitRepository: %w", err)
} else if err = createDelegateHooks(repoPath); err != nil {
return fmt.Errorf("createDelegateHooks: %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 = checkInitRepository(ctx, repo.OwnerName, repo.Name); 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 = 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 := git.OpenRepository(ctx, repo.RepoPath())
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 err = UpdateRepository(ctx, repo, false); err != nil {
return fmt.Errorf("updateRepository: %w", err)
}
return nil
}
// InitializeLabels adds a label set to a repository using a template
func InitializeLabels(ctx context.Context, id int64, labelTemplate string, isOrg bool) error {
list, err := label.GetTemplateFile(labelTemplate)
if err != nil {
return err
}
labels := make([]*issues_model.Label, len(list))
for i := 0; i < len(list); i++ {
labels[i] = &issues_model.Label{
Name: list[i].Name,
Exclusive: list[i].Exclusive,
Description: list[i].Description,
Color: list[i].Color,
}
if isOrg {
labels[i].OrgID = id
} else {
labels[i].RepoID = id
}
}
for _, label := range labels {
if err = issues_model.NewLabel(ctx, label); err != nil {
return err
}
}
return nil
}