Variable expansion in repository templates (#9163)
* Start expansion Signed-off-by: jolheiser <john.olheiser@gmail.com> * _template rather than .template Signed-off-by: jolheiser <john.olheiser@gmail.com> * Use ioutil Signed-off-by: jolheiser <john.olheiser@gmail.com> * Add descriptions to mapping * Start globbing Signed-off-by: jolheiser <john.olheiser@gmail.com> * Tune globbing Signed-off-by: jolheiser <john.olheiser@gmail.com> * Re-arrange imports Signed-off-by: jolheiser <john.olheiser@gmail.com> * Don't expand git hooks Signed-off-by: jolheiser <john.olheiser@gmail.com> * Add glob tests for .giteatemplate Signed-off-by: jolheiser <john.olheiser@gmail.com> * Parse globs separately so they can be tested more easily Signed-off-by: jolheiser <john.olheiser@gmail.com> * Change template location and add docs Signed-off-by: jolheiser <john.olheiser@gmail.com> * nit Signed-off-by: jolheiser <john.olheiser@gmail.com> * Update docs/content/doc/features/gitea-directory.md Co-Authored-By: guillep2k <18600385+guillep2k@users.noreply.github.com> * Update docs/content/doc/features/gitea-directory.md Co-Authored-By: guillep2k <18600385+guillep2k@users.noreply.github.com> * Add upper-lower case match Signed-off-by: jolheiser <john.olheiser@gmail.com> * Nits Signed-off-by: jolheiser <john.olheiser@gmail.com> * Update models/repo_generate.go Co-Authored-By: guillep2k <18600385+guillep2k@users.noreply.github.com>
This commit is contained in:
parent
c9d50bcab5
commit
15a5c10d33
5 changed files with 298 additions and 51 deletions
56
docs/content/doc/features/gitea-directory.md
Normal file
56
docs/content/doc/features/gitea-directory.md
Normal file
|
@ -0,0 +1,56 @@
|
||||||
|
---
|
||||||
|
date: "2019-11-28:00:00+02:00"
|
||||||
|
title: "The .gitea Directory"
|
||||||
|
slug: "gitea-directory"
|
||||||
|
weight: 40
|
||||||
|
toc: true
|
||||||
|
draft: false
|
||||||
|
menu:
|
||||||
|
sidebar:
|
||||||
|
parent: "features"
|
||||||
|
name: "The .gitea Directory"
|
||||||
|
weight: 50
|
||||||
|
identifier: "gitea-directory"
|
||||||
|
---
|
||||||
|
|
||||||
|
# The .gitea directory
|
||||||
|
Gitea repositories can include a `.gitea` directory at their base which will store settings/configurations for certain features.
|
||||||
|
|
||||||
|
## Templates
|
||||||
|
Gitea includes template repositories, and one feature implemented with them is auto-expansion of specific variables within your template files.
|
||||||
|
To tell Gitea which files to expand, you must include a `template` file inside the `.gitea` directory of the template repository.
|
||||||
|
Gitea uses [gobwas/glob](https://github.com/gobwas/glob) for its glob syntax. It closely resembles a traditional `.gitignore`, however there may be slight differences.
|
||||||
|
|
||||||
|
### Example `.gitea/template` file
|
||||||
|
All paths are relative to the base of the repository
|
||||||
|
```gitignore
|
||||||
|
# All .go files, anywhere in the repository
|
||||||
|
**.go
|
||||||
|
|
||||||
|
# All text files in the text directory
|
||||||
|
text/*.txt
|
||||||
|
|
||||||
|
# A specific file
|
||||||
|
a/b/c/d.json
|
||||||
|
|
||||||
|
# Batch files in both upper or lower case can be matched
|
||||||
|
**.[bB][aA][tT]
|
||||||
|
```
|
||||||
|
**NOTE:** The `template` file will be removed from the `.gitea` directory when a repository is generated from the template.
|
||||||
|
|
||||||
|
### Variable Expansion
|
||||||
|
In any file matched by the above globs, certain variables will be expanded.
|
||||||
|
All variables must be of the form `$VAR` or `${VAR}`. To escape an expansion, use a double `$$`, such as `$$VAR` or `$${VAR}`
|
||||||
|
|
||||||
|
| Variable | Expands To |
|
||||||
|
|----------------------|-----------------------------------------------------|
|
||||||
|
| REPO_NAME | The name of the generated repository |
|
||||||
|
| TEMPLATE_NAME | The name of the template repository |
|
||||||
|
| REPO_DESCRIPTION | The description of the generated repository |
|
||||||
|
| TEMPLATE_DESCRIPTION | The description of the template repository |
|
||||||
|
| REPO_LINK | The URL to the generated repository |
|
||||||
|
| TEMPLATE_LINK | The URL to the template repository |
|
||||||
|
| REPO_HTTPS_URL | The HTTP(S) clone link for the generated repository |
|
||||||
|
| TEMPLATE_HTTPS_URL | The HTTP(S) clone link for the template repository |
|
||||||
|
| REPO_SSH_URL | The SSH clone link for the generated repository |
|
||||||
|
| TEMPLATE_SSH_URL | The SSH clone link for the template repository |
|
|
@ -1361,54 +1361,6 @@ func prepareRepoCommit(e Engine, repo *Repository, tmpDir, repoPath string, opts
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func generateRepoCommit(e Engine, repo, templateRepo *Repository, tmpDir string) 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.
|
|
||||||
templateRepoPath := templateRepo.repoPath(e)
|
|
||||||
_, stderr, err := process.GetManager().ExecDirEnv(
|
|
||||||
-1, "",
|
|
||||||
fmt.Sprintf("generateRepoCommit(git clone): %s", templateRepoPath),
|
|
||||||
env,
|
|
||||||
git.GitExecutable, "clone", "--depth", "1", templateRepoPath, tmpDir,
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("git clone: %v - %s", err, stderr)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := os.RemoveAll(path.Join(tmpDir, ".git")); err != nil {
|
|
||||||
return fmt.Errorf("remove git dir: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := git.InitRepository(tmpDir, false); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
repoPath := repo.repoPath(e)
|
|
||||||
_, stderr, err = process.GetManager().ExecDirEnv(
|
|
||||||
-1, tmpDir,
|
|
||||||
fmt.Sprintf("generateRepoCommit(git remote add): %s", repoPath),
|
|
||||||
env,
|
|
||||||
git.GitExecutable, "remote", "add", "origin", repoPath,
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("git remote add: %v - %s", err, stderr)
|
|
||||||
}
|
|
||||||
|
|
||||||
return initRepoCommit(tmpDir, repo.Owner)
|
|
||||||
}
|
|
||||||
|
|
||||||
func checkInitRepository(repoPath string) (err error) {
|
func checkInitRepository(repoPath string) (err error) {
|
||||||
// Somehow the directory could exist.
|
// Somehow the directory could exist.
|
||||||
if com.IsExist(repoPath) {
|
if com.IsExist(repoPath) {
|
||||||
|
|
|
@ -6,7 +6,9 @@ package models
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"io/ioutil"
|
||||||
"os"
|
"os"
|
||||||
|
"path"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
@ -14,7 +16,10 @@ import (
|
||||||
|
|
||||||
"code.gitea.io/gitea/modules/git"
|
"code.gitea.io/gitea/modules/git"
|
||||||
"code.gitea.io/gitea/modules/log"
|
"code.gitea.io/gitea/modules/log"
|
||||||
|
"code.gitea.io/gitea/modules/process"
|
||||||
|
"code.gitea.io/gitea/modules/util"
|
||||||
|
|
||||||
|
"github.com/gobwas/glob"
|
||||||
"github.com/unknwon/com"
|
"github.com/unknwon/com"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -36,8 +41,148 @@ func (gro GenerateRepoOptions) IsValid() bool {
|
||||||
return gro.GitContent || gro.Topics || gro.GitHooks || gro.Webhooks || gro.Avatar || gro.IssueLabels // or other items as they are added
|
return gro.GitContent || gro.Topics || gro.GitHooks || gro.Webhooks || gro.Avatar || gro.IssueLabels // or other items as they are added
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GiteaTemplate holds information about a .gitea/template file
|
||||||
|
type GiteaTemplate struct {
|
||||||
|
Path string
|
||||||
|
Content []byte
|
||||||
|
|
||||||
|
globs []glob.Glob
|
||||||
|
}
|
||||||
|
|
||||||
|
// Globs parses the .gitea/template globs or returns them if they were already parsed
|
||||||
|
func (gt GiteaTemplate) Globs() []glob.Glob {
|
||||||
|
if gt.globs != nil {
|
||||||
|
return gt.globs
|
||||||
|
}
|
||||||
|
|
||||||
|
gt.globs = make([]glob.Glob, 0)
|
||||||
|
lines := strings.Split(string(util.NormalizeEOL(gt.Content)), "\n")
|
||||||
|
for _, line := range lines {
|
||||||
|
line = strings.TrimSpace(line)
|
||||||
|
if line == "" || strings.HasPrefix(line, "#") {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
g, err := glob.Compile(line, '/')
|
||||||
|
if err != nil {
|
||||||
|
log.Info("Invalid glob expression '%s' (skipped): %v", line, err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
gt.globs = append(gt.globs, g)
|
||||||
|
}
|
||||||
|
return gt.globs
|
||||||
|
}
|
||||||
|
|
||||||
|
func checkGiteaTemplate(tmpDir string) (*GiteaTemplate, error) {
|
||||||
|
gtPath := filepath.Join(tmpDir, ".gitea", "template")
|
||||||
|
if _, err := os.Stat(gtPath); os.IsNotExist(err) {
|
||||||
|
return nil, nil
|
||||||
|
} else if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
content, err := ioutil.ReadFile(gtPath)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
gt := &GiteaTemplate{
|
||||||
|
Path: gtPath,
|
||||||
|
Content: content,
|
||||||
|
}
|
||||||
|
|
||||||
|
return gt, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func generateRepoCommit(e Engine, repo, templateRepo, generateRepo *Repository, tmpDir string) 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.
|
||||||
|
templateRepoPath := templateRepo.repoPath(e)
|
||||||
|
if err := git.Clone(templateRepoPath, tmpDir, git.CloneRepoOptions{
|
||||||
|
Depth: 1,
|
||||||
|
}); err != nil {
|
||||||
|
return fmt.Errorf("git clone: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := os.RemoveAll(path.Join(tmpDir, ".git")); err != nil {
|
||||||
|
return fmt.Errorf("remove git dir: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Variable expansion
|
||||||
|
gt, err := checkGiteaTemplate(tmpDir)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("checkGiteaTemplate: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := os.Remove(gt.Path); err != nil {
|
||||||
|
return fmt.Errorf("remove .giteatemplate: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Avoid walking tree if there are no globs
|
||||||
|
if len(gt.Globs()) > 0 {
|
||||||
|
tmpDirSlash := strings.TrimSuffix(filepath.ToSlash(tmpDir), "/") + "/"
|
||||||
|
if err := filepath.Walk(tmpDirSlash, func(path string, info os.FileInfo, walkErr error) error {
|
||||||
|
if walkErr != nil {
|
||||||
|
return walkErr
|
||||||
|
}
|
||||||
|
|
||||||
|
if info.IsDir() {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
base := strings.TrimPrefix(filepath.ToSlash(path), tmpDirSlash)
|
||||||
|
for _, g := range gt.Globs() {
|
||||||
|
if g.Match(base) {
|
||||||
|
content, err := ioutil.ReadFile(path)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := ioutil.WriteFile(path,
|
||||||
|
[]byte(generateExpansion(string(content), templateRepo, generateRepo)),
|
||||||
|
0644); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := git.InitRepository(tmpDir, false); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
repoPath := repo.repoPath(e)
|
||||||
|
_, stderr, err := process.GetManager().ExecDirEnv(
|
||||||
|
-1, tmpDir,
|
||||||
|
fmt.Sprintf("generateRepoCommit(git remote add): %s", repoPath),
|
||||||
|
env,
|
||||||
|
git.GitExecutable, "remote", "add", "origin", repoPath,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("git remote add: %v - %s", err, stderr)
|
||||||
|
}
|
||||||
|
|
||||||
|
return initRepoCommit(tmpDir, repo.Owner)
|
||||||
|
}
|
||||||
|
|
||||||
// generateRepository initializes repository from template
|
// generateRepository initializes repository from template
|
||||||
func generateRepository(e Engine, repo, templateRepo *Repository) (err error) {
|
func generateRepository(e Engine, repo, templateRepo, generateRepo *Repository) (err error) {
|
||||||
tmpDir := filepath.Join(os.TempDir(), "gitea-"+repo.Name+"-"+com.ToStr(time.Now().Nanosecond()))
|
tmpDir := filepath.Join(os.TempDir(), "gitea-"+repo.Name+"-"+com.ToStr(time.Now().Nanosecond()))
|
||||||
|
|
||||||
if err := os.MkdirAll(tmpDir, os.ModePerm); err != nil {
|
if err := os.MkdirAll(tmpDir, os.ModePerm); err != nil {
|
||||||
|
@ -50,7 +195,7 @@ func generateRepository(e Engine, repo, templateRepo *Repository) (err error) {
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
if err = generateRepoCommit(e, repo, templateRepo, tmpDir); err != nil {
|
if err = generateRepoCommit(e, repo, templateRepo, generateRepo, tmpDir); err != nil {
|
||||||
return fmt.Errorf("generateRepoCommit: %v", err)
|
return fmt.Errorf("generateRepoCommit: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -95,7 +240,7 @@ func GenerateRepository(ctx DBContext, doer, owner *User, templateRepo *Reposito
|
||||||
|
|
||||||
// GenerateGitContent generates git content from a template repository
|
// GenerateGitContent generates git content from a template repository
|
||||||
func GenerateGitContent(ctx DBContext, templateRepo, generateRepo *Repository) error {
|
func GenerateGitContent(ctx DBContext, templateRepo, generateRepo *Repository) error {
|
||||||
if err := generateRepository(ctx.e, generateRepo, templateRepo); err != nil {
|
if err := generateRepository(ctx.e, generateRepo, templateRepo, generateRepo); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -210,3 +355,36 @@ func GenerateIssueLabels(ctx DBContext, templateRepo, generateRepo *Repository)
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func generateExpansion(src string, templateRepo, generateRepo *Repository) string {
|
||||||
|
return os.Expand(src, func(key string) string {
|
||||||
|
switch key {
|
||||||
|
case "REPO_NAME":
|
||||||
|
return generateRepo.Name
|
||||||
|
case "TEMPLATE_NAME":
|
||||||
|
return templateRepo.Name
|
||||||
|
case "REPO_DESCRIPTION":
|
||||||
|
return generateRepo.Description
|
||||||
|
case "TEMPLATE_DESCRIPTION":
|
||||||
|
return templateRepo.Description
|
||||||
|
case "REPO_OWNER":
|
||||||
|
return generateRepo.MustOwnerName()
|
||||||
|
case "TEMPLATE_OWNER":
|
||||||
|
return templateRepo.MustOwnerName()
|
||||||
|
case "REPO_LINK":
|
||||||
|
return generateRepo.Link()
|
||||||
|
case "TEMPLATE_LINK":
|
||||||
|
return templateRepo.Link()
|
||||||
|
case "REPO_HTTPS_URL":
|
||||||
|
return generateRepo.CloneLink().HTTPS
|
||||||
|
case "TEMPLATE_HTTPS_URL":
|
||||||
|
return templateRepo.CloneLink().HTTPS
|
||||||
|
case "REPO_SSH_URL":
|
||||||
|
return generateRepo.CloneLink().SSH
|
||||||
|
case "TEMPLATE_SSH_URL":
|
||||||
|
return templateRepo.CloneLink().SSH
|
||||||
|
default:
|
||||||
|
return key
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
57
models/repo_generate_test.go
Normal file
57
models/repo_generate_test.go
Normal file
|
@ -0,0 +1,57 @@
|
||||||
|
// Copyright 2019 The Gitea Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a MIT-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
package models
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
var giteaTemplate = []byte(`
|
||||||
|
# Header
|
||||||
|
|
||||||
|
# All .go files
|
||||||
|
**.go
|
||||||
|
|
||||||
|
# All text files in /text/
|
||||||
|
text/*.txt
|
||||||
|
|
||||||
|
# All files in modules folders
|
||||||
|
**/modules/*
|
||||||
|
`)
|
||||||
|
|
||||||
|
func TestGiteaTemplate(t *testing.T) {
|
||||||
|
gt := GiteaTemplate{Content: giteaTemplate}
|
||||||
|
assert.Equal(t, len(gt.Globs()), 3)
|
||||||
|
|
||||||
|
tt := []struct {
|
||||||
|
Path string
|
||||||
|
Match bool
|
||||||
|
}{
|
||||||
|
{Path: "main.go", Match: true},
|
||||||
|
{Path: "a/b/c/d/e.go", Match: true},
|
||||||
|
{Path: "main.txt", Match: false},
|
||||||
|
{Path: "a/b.txt", Match: false},
|
||||||
|
{Path: "text/a.txt", Match: true},
|
||||||
|
{Path: "text/b.txt", Match: true},
|
||||||
|
{Path: "text/c.json", Match: false},
|
||||||
|
{Path: "a/b/c/modules/README.md", Match: true},
|
||||||
|
{Path: "a/b/c/modules/d/README.md", Match: false},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range tt {
|
||||||
|
t.Run(tc.Path, func(t *testing.T) {
|
||||||
|
match := false
|
||||||
|
for _, g := range gt.Globs() {
|
||||||
|
if g.Match(tc.Path) {
|
||||||
|
match = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
assert.Equal(t, tc.Match, match)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
|
@ -161,6 +161,7 @@ type CloneRepoOptions struct {
|
||||||
Branch string
|
Branch string
|
||||||
Shared bool
|
Shared bool
|
||||||
NoCheckout bool
|
NoCheckout bool
|
||||||
|
Depth int
|
||||||
}
|
}
|
||||||
|
|
||||||
// Clone clones original repository to target path.
|
// Clone clones original repository to target path.
|
||||||
|
@ -193,6 +194,9 @@ func CloneWithArgs(from, to string, args []string, opts CloneRepoOptions) (err e
|
||||||
if opts.NoCheckout {
|
if opts.NoCheckout {
|
||||||
cmd.AddArguments("--no-checkout")
|
cmd.AddArguments("--no-checkout")
|
||||||
}
|
}
|
||||||
|
if opts.Depth > 0 {
|
||||||
|
cmd.AddArguments("--depth", strconv.Itoa(opts.Depth))
|
||||||
|
}
|
||||||
|
|
||||||
if len(opts.Branch) > 0 {
|
if len(opts.Branch) > 0 {
|
||||||
cmd.AddArguments("-b", opts.Branch)
|
cmd.AddArguments("-b", opts.Branch)
|
||||||
|
|
Reference in a new issue