2020-01-12 20:11:17 +08:00
// Copyright 2019 The Gitea Authors. All rights reserved.
2022-11-27 13:20:29 -05:00
// SPDX-License-Identifier: MIT
2020-01-12 20:11:17 +08:00
package repository
import (
2022-06-06 16:01:49 +08:00
"bufio"
"bytes"
2021-09-23 16:45:36 +01:00
"context"
2025-11-12 19:44:49 +08:00
"errors"
2020-01-12 20:11:17 +08:00
"fmt"
2025-11-12 19:44:49 +08:00
"io/fs"
2020-01-12 20:11:17 +08:00
"os"
"path/filepath"
2023-06-20 17:14:47 -04:00
"regexp"
2024-07-09 20:05:12 +02:00
"strconv"
2020-01-12 20:11:17 +08:00
"strings"
2025-10-21 02:43:08 +08:00
"sync"
2020-01-12 20:11:17 +08:00
"time"
2022-06-12 23:51:54 +08:00
git_model "code.gitea.io/gitea/models/git"
2021-12-10 09:27:50 +08:00
repo_model "code.gitea.io/gitea/models/repo"
2020-01-12 20:11:17 +08:00
"code.gitea.io/gitea/modules/git"
2024-01-28 04:09:51 +08:00
"code.gitea.io/gitea/modules/gitrepo"
2025-09-14 02:01:00 +08:00
"code.gitea.io/gitea/modules/glob"
2020-01-12 20:11:17 +08:00
"code.gitea.io/gitea/modules/log"
2024-02-28 21:40:36 +08:00
repo_module "code.gitea.io/gitea/modules/repository"
2025-04-08 09:15:28 -07:00
"code.gitea.io/gitea/modules/setting"
2020-08-11 21:05:34 +01:00
"code.gitea.io/gitea/modules/util"
2020-01-28 07:57:15 -06:00
"github.com/huandu/xstrings"
2020-01-12 20:11:17 +08:00
)
2020-01-28 07:57:15 -06:00
type transformer struct {
Name string
Transform func ( string ) string
}
type expansion struct {
Name string
Value string
Transformers [ ] transformer
}
2025-10-21 02:43:08 +08:00
var globalVars = sync . OnceValue ( func ( ) ( ret struct {
defaultTransformers [ ] transformer
fileNameSanitizeRegexp * regexp . Regexp
} ,
) {
ret . defaultTransformers = [ ] transformer {
{ Name : "SNAKE" , Transform : xstrings . ToSnakeCase } ,
{ Name : "KEBAB" , Transform : xstrings . ToKebabCase } ,
{ Name : "CAMEL" , Transform : xstrings . ToCamelCase } ,
{ Name : "PASCAL" , Transform : xstrings . ToPascalCase } ,
{ Name : "LOWER" , Transform : strings . ToLower } ,
{ Name : "UPPER" , Transform : strings . ToUpper } ,
{ Name : "TITLE" , Transform : util . ToTitleCase } ,
}
// invalid filename contents, based on https://github.com/sindresorhus/filename-reserved-regex
// "COM10" needs to be opened with UNC "\\.\COM10" on Windows, so itself is valid
ret . fileNameSanitizeRegexp = regexp . MustCompile ( ` (?i)[<>:"/\\|?*\x { 0000}-\x { 001F}]|^(con|prn|aux|nul|com\d|lpt\d)$ ` )
return ret
} )
2020-01-28 07:57:15 -06:00
2025-10-21 02:43:08 +08:00
func generateExpansion ( ctx context . Context , src string , templateRepo , generateRepo * repo_model . Repository ) string {
transformers := globalVars ( ) . defaultTransformers
2024-07-09 20:05:12 +02:00
year , month , day := time . Now ( ) . Date ( )
2020-01-28 07:57:15 -06:00
expansions := [ ] expansion {
2024-07-09 20:05:12 +02:00
{ Name : "YEAR" , Value : strconv . Itoa ( year ) , Transformers : nil } ,
{ Name : "MONTH" , Value : fmt . Sprintf ( "%02d" , int ( month ) ) , Transformers : nil } ,
2025-10-21 02:43:08 +08:00
{ Name : "MONTH_ENGLISH" , Value : month . String ( ) , Transformers : transformers } ,
2024-07-09 20:05:12 +02:00
{ Name : "DAY" , Value : fmt . Sprintf ( "%02d" , day ) , Transformers : nil } ,
2025-10-21 02:43:08 +08:00
{ Name : "REPO_NAME" , Value : generateRepo . Name , Transformers : transformers } ,
{ Name : "TEMPLATE_NAME" , Value : templateRepo . Name , Transformers : transformers } ,
2020-01-28 07:57:15 -06:00
{ Name : "REPO_DESCRIPTION" , Value : generateRepo . Description , Transformers : nil } ,
{ Name : "TEMPLATE_DESCRIPTION" , Value : templateRepo . Description , Transformers : nil } ,
2025-10-21 02:43:08 +08:00
{ Name : "REPO_OWNER" , Value : generateRepo . OwnerName , Transformers : transformers } ,
{ Name : "TEMPLATE_OWNER" , Value : templateRepo . OwnerName , Transformers : transformers } ,
2020-01-28 07:57:15 -06:00
{ Name : "REPO_LINK" , Value : generateRepo . Link ( ) , Transformers : nil } ,
{ Name : "TEMPLATE_LINK" , Value : templateRepo . Link ( ) , Transformers : nil } ,
2025-01-07 13:17:44 +08:00
{ Name : "REPO_HTTPS_URL" , Value : generateRepo . CloneLinkGeneral ( ctx ) . HTTPS , Transformers : nil } ,
{ Name : "TEMPLATE_HTTPS_URL" , Value : templateRepo . CloneLinkGeneral ( ctx ) . HTTPS , Transformers : nil } ,
{ Name : "REPO_SSH_URL" , Value : generateRepo . CloneLinkGeneral ( ctx ) . SSH , Transformers : nil } ,
{ Name : "TEMPLATE_SSH_URL" , Value : templateRepo . CloneLinkGeneral ( ctx ) . SSH , Transformers : nil } ,
2020-01-28 07:57:15 -06:00
}
2022-01-20 18:46:10 +01:00
expansionMap := make ( map [ string ] string )
2020-01-28 07:57:15 -06:00
for _ , e := range expansions {
expansionMap [ e . Name ] = e . Value
for _ , tr := range e . Transformers {
expansionMap [ fmt . Sprintf ( "%s_%s" , e . Name , tr . Name ) ] = tr . Transform ( e . Value )
}
}
2020-01-12 20:11:17 +08:00
return os . Expand ( src , func ( key string ) string {
2025-10-21 02:43:08 +08:00
if val , ok := expansionMap [ key ] ; ok {
return val
2020-01-12 20:11:17 +08:00
}
2020-01-28 07:57:15 -06:00
return key
2020-01-12 20:11:17 +08:00
} )
}
2025-10-21 02:43:08 +08:00
// giteaTemplateFileMatcher holds information about a .gitea/template file
type giteaTemplateFileMatcher struct {
2026-02-25 09:21:07 +08:00
relPath string
globs [ ] glob . Glob
2022-06-06 16:01:49 +08:00
}
2026-02-25 09:21:07 +08:00
func newGiteaTemplateFileMatcher ( relPath string , content [ ] byte ) * giteaTemplateFileMatcher {
gt := & giteaTemplateFileMatcher { relPath : relPath }
2022-06-06 16:01:49 +08:00
gt . globs = make ( [ ] glob . Glob , 0 )
2025-10-21 02:43:08 +08:00
scanner := bufio . NewScanner ( bytes . NewReader ( content ) )
2022-06-06 16:01:49 +08:00
for scanner . Scan ( ) {
line := strings . TrimSpace ( scanner . Text ( ) )
if line == "" || strings . HasPrefix ( line , "#" ) {
continue
}
g , err := glob . Compile ( line , '/' )
if err != nil {
2025-10-21 02:43:08 +08:00
log . Debug ( "Invalid glob expression '%s' (skipped): %v" , line , err )
2022-06-06 16:01:49 +08:00
continue
}
gt . globs = append ( gt . globs , g )
}
2025-10-21 02:43:08 +08:00
return gt
}
func ( gt * giteaTemplateFileMatcher ) HasRules ( ) bool {
return len ( gt . globs ) != 0
2022-06-06 16:01:49 +08:00
}
2025-10-21 02:43:08 +08:00
func ( gt * giteaTemplateFileMatcher ) Match ( s string ) bool {
for _ , g := range gt . globs {
if g . Match ( s ) {
return true
}
}
return false
}
2025-11-12 19:44:49 +08:00
func readGiteaTemplateFile ( tmpDir string ) ( * giteaTemplateFileMatcher , error ) {
2026-02-25 09:21:07 +08:00
templateRelPath := filepath . Join ( ".gitea" , "template" )
content , err := util . ReadRegularPathFile ( tmpDir , templateRelPath , 1024 * 1024 )
2025-11-12 19:44:49 +08:00
if err != nil {
2026-02-25 09:21:07 +08:00
return nil , util . Iif ( errors . Is ( err , util . ErrNotRegularPathFile ) , os . ErrNotExist , err )
2025-11-12 19:44:49 +08:00
}
2026-02-25 09:21:07 +08:00
return newGiteaTemplateFileMatcher ( templateRelPath , content ) , nil
2025-01-01 03:55:13 +01:00
}
2025-10-21 02:43:08 +08:00
func substGiteaTemplateFile ( ctx context . Context , tmpDir , tmpDirSubPath string , templateRepo , generateRepo * repo_model . Repository ) error {
2026-02-25 09:21:07 +08:00
content , err := util . ReadRegularPathFile ( tmpDir , tmpDirSubPath , 1024 * 1024 )
2025-10-21 02:43:08 +08:00
if err != nil {
2026-02-25 09:21:07 +08:00
if errors . Is ( err , fs . ErrNotExist ) {
return nil
}
return err
2025-10-21 02:43:08 +08:00
}
2026-02-25 09:21:07 +08:00
if err := os . Remove ( util . FilePathJoinAbs ( tmpDir , tmpDirSubPath ) ) ; err != nil {
2025-10-21 02:43:08 +08:00
return err
2020-01-12 20:11:17 +08:00
}
2025-10-21 02:43:08 +08:00
generatedContent := generateExpansion ( ctx , string ( content ) , templateRepo , generateRepo )
2025-12-13 02:56:05 +08:00
substSubPath := filePathSanitize ( generateExpansion ( ctx , tmpDirSubPath , templateRepo , generateRepo ) )
2026-02-25 09:21:07 +08:00
return util . WriteRegularPathFile ( tmpDir , substSubPath , [ ] byte ( generatedContent ) , 0 o755 , 0 o644 )
2025-10-21 02:43:08 +08:00
}
2026-02-25 09:21:07 +08:00
// processGiteaTemplateFile processes and removes the .gitea/template file, does variable expansion for template files
// and save the processed files to the filesystem. It returns a list of skipped files that are not regular paths.
func processGiteaTemplateFile ( ctx context . Context , tmpDir string , templateRepo , generateRepo * repo_model . Repository , fileMatcher * giteaTemplateFileMatcher ) ( skippedFiles [ ] string , _ error ) {
// Why not use "os.Root" here: symlink is unsafe even in the same root but "os.Root" can't help, it's more difficult to use "os.Root" to do the WalkDir.
if err := os . Remove ( util . FilePathJoinAbs ( tmpDir , fileMatcher . relPath ) ) ; err != nil {
return nil , fmt . Errorf ( "unable to remove .gitea/template: %w" , err )
2025-10-21 02:43:08 +08:00
}
if ! fileMatcher . HasRules ( ) {
2026-02-25 09:21:07 +08:00
return skippedFiles , nil // Avoid walking tree if there are no globs
2025-01-01 03:55:13 +01:00
}
2025-10-21 02:43:08 +08:00
2026-02-25 09:21:07 +08:00
err := filepath . WalkDir ( tmpDir , func ( fullPath string , d os . DirEntry , walkErr error ) error {
2025-01-01 03:55:13 +01:00
if walkErr != nil {
return walkErr
}
if d . IsDir ( ) {
return nil
}
2025-10-21 02:43:08 +08:00
tmpDirSubPath , err := filepath . Rel ( tmpDir , fullPath )
if err != nil {
return err
}
if fileMatcher . Match ( filepath . ToSlash ( tmpDirSubPath ) ) {
2026-02-25 09:21:07 +08:00
err := substGiteaTemplateFile ( ctx , tmpDir , tmpDirSubPath , templateRepo , generateRepo )
if errors . Is ( err , util . ErrNotRegularPathFile ) {
skippedFiles = append ( skippedFiles , tmpDirSubPath )
} else if err != nil {
return err
}
2025-01-01 03:55:13 +01:00
}
return nil
} ) // end: WalkDir
2026-02-25 09:21:07 +08:00
if err != nil {
return nil , err
}
if err = util . RemoveAll ( util . FilePathJoinAbs ( tmpDir , ".git" ) ) ; err != nil {
return nil , err
}
return skippedFiles , nil
2020-01-12 20:11:17 +08:00
}
2022-01-19 23:26:57 +00:00
func generateRepoCommit ( ctx context . Context , repo , templateRepo , generateRepo * repo_model . Repository , tmpDir string ) error {
2026-05-16 01:54:48 +08:00
// set default branch based on whether it's specified in the newly generated repo or not
repo . DefaultBranch = util . IfZero ( repo . DefaultBranch , templateRepo . DefaultBranch )
2020-01-12 20:11:17 +08:00
// Clone to temporary path and do the init commit.
2025-12-10 09:41:01 -08:00
if err := gitrepo . CloneRepoToLocal ( ctx , templateRepo , tmpDir , git . CloneRepoOptions {
2020-03-26 19:14:51 +00:00
Depth : 1 ,
Branch : templateRepo . DefaultBranch ,
2020-01-12 20:11:17 +08:00
} ) ; err != nil {
2022-10-24 21:29:17 +02:00
return fmt . Errorf ( "git clone: %w" , err )
2020-01-12 20:11:17 +08:00
}
2025-01-01 03:55:13 +01:00
// Get active submodules from the template
submodules , err := git . GetTemplateSubmoduleCommits ( ctx , tmpDir )
if err != nil {
return fmt . Errorf ( "GetTemplateSubmoduleCommits: %w" , err )
}
if err = util . RemoveAll ( filepath . Join ( tmpDir , ".git" ) ) ; err != nil {
2022-10-24 21:29:17 +02:00
return fmt . Errorf ( "remove git dir: %w" , err )
2020-01-12 20:11:17 +08:00
}
// Variable expansion
2025-10-21 02:43:08 +08:00
fileMatcher , err := readGiteaTemplateFile ( tmpDir )
2025-11-12 19:44:49 +08:00
if err == nil {
2026-02-25 09:21:07 +08:00
_ , err = processGiteaTemplateFile ( ctx , tmpDir , templateRepo , generateRepo , fileMatcher )
2025-01-01 03:55:13 +01:00
if err != nil {
2025-11-12 19:44:49 +08:00
return fmt . Errorf ( "processGiteaTemplateFile: %w" , err )
2020-01-12 20:11:17 +08:00
}
2025-11-12 19:44:49 +08:00
} else if errors . Is ( err , fs . ErrNotExist ) {
log . Debug ( "skip processing repo template files: no available .gitea/template" )
} else {
return fmt . Errorf ( "readGiteaTemplateFile: %w" , err )
2020-01-12 20:11:17 +08:00
}
2025-01-01 03:55:13 +01:00
if err = git . InitRepository ( ctx , tmpDir , false , templateRepo . ObjectFormatName ) ; err != nil {
2020-01-12 20:11:17 +08:00
return err
}
2025-01-01 03:55:13 +01:00
if err = git . AddTemplateSubmoduleIndexes ( ctx , tmpDir , submodules ) ; err != nil {
return fmt . Errorf ( "failed to add submodules: %v" , err )
}
2026-05-16 01:54:48 +08:00
return initRepoCommit ( ctx , tmpDir , repo , repo . Owner , repo . DefaultBranch )
2020-01-12 20:11:17 +08:00
}
2025-06-18 00:54:25 +08:00
// GenerateGitContent generates git content from a template repository
func GenerateGitContent ( ctx context . Context , templateRepo , generateRepo * repo_model . Repository ) ( err error ) {
tmpDir , cleanup , err := setting . AppDataTempDir ( "git-repo-content" ) . MkdirTempRandom ( "gitea-" + generateRepo . Name )
2020-01-12 20:11:17 +08:00
if err != nil {
2025-06-18 00:54:25 +08:00
return fmt . Errorf ( "failed to create temp dir for repository %s: %w" , generateRepo . FullName ( ) , err )
2020-01-12 20:11:17 +08:00
}
2025-04-08 09:15:28 -07:00
defer cleanup ( )
2020-01-12 20:11:17 +08:00
2025-06-18 00:54:25 +08:00
if err = generateRepoCommit ( ctx , generateRepo , templateRepo , generateRepo , tmpDir ) ; err != nil {
2022-10-24 21:29:17 +02:00
return fmt . Errorf ( "generateRepoCommit: %w" , err )
2020-01-12 20:11:17 +08:00
}
2025-06-18 00:54:25 +08:00
if err = gitrepo . SetDefaultBranch ( ctx , generateRepo , generateRepo . DefaultBranch ) ; err != nil {
2022-10-24 21:29:17 +02:00
return fmt . Errorf ( "setDefaultBranch: %w" , err )
2020-12-11 21:41:59 +00:00
}
2025-06-18 00:54:25 +08:00
if err = repo_model . UpdateRepositoryColsNoAutoTime ( ctx , generateRepo , "default_branch" ) ; err != nil {
2022-10-24 21:29:17 +02:00
return fmt . Errorf ( "updateRepository: %w" , err )
2020-01-12 20:11:17 +08:00
}
2024-02-28 21:40:36 +08:00
if err := repo_module . UpdateRepoSize ( ctx , generateRepo ) ; err != nil {
2022-10-24 21:29:17 +02:00
return fmt . Errorf ( "failed to update size for repository: %w" , err )
2020-01-12 20:11:17 +08:00
}
2022-06-12 23:51:54 +08:00
if err := git_model . CopyLFS ( ctx , generateRepo , templateRepo ) ; err != nil {
2022-10-24 21:29:17 +02:00
return fmt . Errorf ( "failed to copy LFS: %w" , err )
2020-01-12 20:11:17 +08:00
}
2026-05-16 01:54:48 +08:00
if _ , err := repo_module . SyncRepoBranches ( ctx , generateRepo . ID , 0 ) ; err != nil {
return fmt . Errorf ( "SyncRepoBranches: %w" , err )
}
2020-01-12 20:11:17 +08:00
return nil
}
2022-06-06 16:01:49 +08:00
// GenerateRepoOptions contains the template units to generate
type GenerateRepoOptions struct {
2023-07-21 12:32:47 +08:00
Name string
DefaultBranch string
Description string
Private bool
GitContent bool
Topics bool
GitHooks bool
Webhooks bool
Avatar bool
IssueLabels bool
ProtectedBranch bool
2022-06-06 16:01:49 +08:00
}
// IsValid checks whether at least one option is chosen for generation
func ( gro GenerateRepoOptions ) IsValid ( ) bool {
2023-07-21 12:32:47 +08:00
return gro . GitContent || gro . Topics || gro . GitHooks || gro . Webhooks || gro . Avatar ||
gro . IssueLabels || gro . ProtectedBranch // or other items as they are added
2022-06-06 16:01:49 +08:00
}
2025-10-21 02:43:08 +08:00
func filePathSanitize ( s string ) string {
fields := strings . Split ( filepath . ToSlash ( s ) , "/" )
for i , field := range fields {
field = strings . TrimSpace ( strings . TrimSpace ( globalVars ( ) . fileNameSanitizeRegexp . ReplaceAllString ( field , "_" ) ) )
if strings . HasPrefix ( field , ".." ) {
field = "__" + field [ 2 : ]
}
if strings . EqualFold ( field , ".git" ) {
field = "_" + field [ 1 : ]
}
fields [ i ] = field
}
2025-12-13 02:56:05 +08:00
return filepath . Clean ( filepath . FromSlash ( strings . Trim ( strings . Join ( fields , "/" ) , "/" ) ) )
2023-06-20 17:14:47 -04:00
}