2020-01-12 15:11:17 +03:00
// Copyright 2019 The Gitea Authors. All rights reserved.
2022-11-27 21:20:29 +03:00
// SPDX-License-Identifier: MIT
2020-01-12 15:11:17 +03:00
package repository
import (
2022-06-06 11:01:49 +03:00
"bufio"
"bytes"
2021-09-23 18:45:36 +03:00
"context"
2020-01-12 15:11:17 +03:00
"fmt"
"os"
"path"
"path/filepath"
"strings"
"time"
2022-06-12 18:51:54 +03:00
git_model "code.gitea.io/gitea/models/git"
2021-12-10 04:27:50 +03:00
repo_model "code.gitea.io/gitea/models/repo"
2021-11-24 12:49:20 +03:00
user_model "code.gitea.io/gitea/models/user"
2020-01-12 15:11:17 +03:00
"code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/log"
2020-08-11 23:05:34 +03:00
"code.gitea.io/gitea/modules/util"
2020-01-28 16:57:15 +03:00
2022-06-06 11:01:49 +03:00
"github.com/gobwas/glob"
2020-01-28 16:57:15 +03:00
"github.com/huandu/xstrings"
2020-01-12 15:11:17 +03:00
)
2020-01-28 16:57:15 +03:00
type transformer struct {
Name string
Transform func ( string ) string
}
type expansion struct {
Name string
Value string
Transformers [ ] transformer
}
var defaultTransformers = [ ] transformer {
{ Name : "SNAKE" , Transform : xstrings . ToSnakeCase } ,
{ Name : "KEBAB" , Transform : xstrings . ToKebabCase } ,
{ Name : "CAMEL" , Transform : func ( str string ) string {
return xstrings . FirstRuneToLower ( xstrings . ToCamelCase ( str ) )
} } ,
{ Name : "PASCAL" , Transform : xstrings . ToCamelCase } ,
{ Name : "LOWER" , Transform : strings . ToLower } ,
{ Name : "UPPER" , Transform : strings . ToUpper } ,
2022-05-11 00:55:54 +03:00
{ Name : "TITLE" , Transform : util . ToTitleCase } ,
2020-01-28 16:57:15 +03:00
}
2021-12-10 04:27:50 +03:00
func generateExpansion ( src string , templateRepo , generateRepo * repo_model . Repository ) string {
2020-01-28 16:57:15 +03:00
expansions := [ ] expansion {
{ Name : "REPO_NAME" , Value : generateRepo . Name , Transformers : defaultTransformers } ,
{ Name : "TEMPLATE_NAME" , Value : templateRepo . Name , Transformers : defaultTransformers } ,
{ Name : "REPO_DESCRIPTION" , Value : generateRepo . Description , Transformers : nil } ,
{ Name : "TEMPLATE_DESCRIPTION" , Value : templateRepo . Description , Transformers : nil } ,
{ Name : "REPO_OWNER" , Value : generateRepo . OwnerName , Transformers : defaultTransformers } ,
{ Name : "TEMPLATE_OWNER" , Value : templateRepo . OwnerName , Transformers : defaultTransformers } ,
{ Name : "REPO_LINK" , Value : generateRepo . Link ( ) , Transformers : nil } ,
{ Name : "TEMPLATE_LINK" , Value : templateRepo . Link ( ) , Transformers : nil } ,
{ Name : "REPO_HTTPS_URL" , Value : generateRepo . CloneLink ( ) . HTTPS , Transformers : nil } ,
{ Name : "TEMPLATE_HTTPS_URL" , Value : templateRepo . CloneLink ( ) . HTTPS , Transformers : nil } ,
{ Name : "REPO_SSH_URL" , Value : generateRepo . CloneLink ( ) . SSH , Transformers : nil } ,
{ Name : "TEMPLATE_SSH_URL" , Value : templateRepo . CloneLink ( ) . SSH , Transformers : nil } ,
}
2022-01-20 20:46:10 +03:00
expansionMap := make ( map [ string ] string )
2020-01-28 16:57:15 +03: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 15:11:17 +03:00
return os . Expand ( src , func ( key string ) string {
2020-01-28 16:57:15 +03:00
if expansion , ok := expansionMap [ key ] ; ok {
return expansion
2020-01-12 15:11:17 +03:00
}
2020-01-28 16:57:15 +03:00
return key
2020-01-12 15:11:17 +03:00
} )
}
2022-06-06 11:01:49 +03:00
// 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 )
scanner := bufio . NewScanner ( bytes . NewReader ( gt . Content ) )
for scanner . Scan ( ) {
line := strings . TrimSpace ( scanner . Text ( ) )
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 ) {
2020-01-12 15:11:17 +03:00
gtPath := filepath . Join ( tmpDir , ".gitea" , "template" )
if _ , err := os . Stat ( gtPath ) ; os . IsNotExist ( err ) {
return nil , nil
} else if err != nil {
return nil , err
}
2021-09-22 08:38:34 +03:00
content , err := os . ReadFile ( gtPath )
2020-01-12 15:11:17 +03:00
if err != nil {
return nil , err
}
2022-06-06 11:01:49 +03:00
gt := & GiteaTemplate {
2020-01-12 15:11:17 +03:00
Path : gtPath ,
Content : content ,
}
return gt , nil
}
2022-01-20 02:26:57 +03:00
func generateRepoCommit ( ctx context . Context , repo , templateRepo , generateRepo * repo_model . Repository , tmpDir string ) error {
2020-01-12 15:11:17 +03:00
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 ( )
2022-01-20 02:26:57 +03:00
if err := git . Clone ( ctx , templateRepoPath , tmpDir , git . CloneRepoOptions {
2020-03-26 22:14:51 +03:00
Depth : 1 ,
Branch : templateRepo . DefaultBranch ,
2020-01-12 15:11:17 +03:00
} ) ; err != nil {
2022-10-24 22:29:17 +03:00
return fmt . Errorf ( "git clone: %w" , err )
2020-01-12 15:11:17 +03:00
}
2020-08-11 23:05:34 +03:00
if err := util . RemoveAll ( path . Join ( tmpDir , ".git" ) ) ; err != nil {
2022-10-24 22:29:17 +03:00
return fmt . Errorf ( "remove git dir: %w" , err )
2020-01-12 15:11:17 +03:00
}
// Variable expansion
gt , err := checkGiteaTemplate ( tmpDir )
if err != nil {
2022-10-24 22:29:17 +03:00
return fmt . Errorf ( "checkGiteaTemplate: %w" , err )
2020-01-12 15:11:17 +03:00
}
2020-01-28 16:57:15 +03:00
if gt != nil {
2020-08-11 23:05:34 +03:00
if err := util . Remove ( gt . Path ) ; err != nil {
2022-10-24 22:29:17 +03:00
return fmt . Errorf ( "remove .giteatemplate: %w" , err )
2020-01-28 16:57:15 +03:00
}
2020-01-12 15:11:17 +03:00
2020-01-28 16:57:15 +03:00
// Avoid walking tree if there are no globs
if len ( gt . Globs ( ) ) > 0 {
tmpDirSlash := strings . TrimSuffix ( filepath . ToSlash ( tmpDir ) , "/" ) + "/"
2023-01-16 19:21:44 +03:00
if err := filepath . WalkDir ( tmpDirSlash , func ( path string , d os . DirEntry , walkErr error ) error {
2020-01-28 16:57:15 +03:00
if walkErr != nil {
return walkErr
}
2020-01-12 15:11:17 +03:00
2023-01-16 19:21:44 +03:00
if d . IsDir ( ) {
2020-01-28 16:57:15 +03:00
return nil
}
2020-01-12 15:11:17 +03:00
2020-01-28 16:57:15 +03:00
base := strings . TrimPrefix ( filepath . ToSlash ( path ) , tmpDirSlash )
for _ , g := range gt . Globs ( ) {
if g . Match ( base ) {
2021-09-22 08:38:34 +03:00
content , err := os . ReadFile ( path )
2020-01-28 16:57:15 +03:00
if err != nil {
return err
}
2021-09-22 08:38:34 +03:00
if err := os . WriteFile ( path ,
2020-01-28 16:57:15 +03:00
[ ] byte ( generateExpansion ( string ( content ) , templateRepo , generateRepo ) ) ,
2022-01-20 20:46:10 +03:00
0 o644 ) ; err != nil {
2020-01-28 16:57:15 +03:00
return err
}
break
2020-01-12 15:11:17 +03:00
}
}
2020-01-28 16:57:15 +03:00
return nil
} ) ; err != nil {
return err
2020-01-12 15:11:17 +03:00
}
}
}
2022-01-20 02:26:57 +03:00
if err := git . InitRepository ( ctx , tmpDir , false ) ; err != nil {
2020-01-12 15:11:17 +03:00
return err
}
repoPath := repo . RepoPath ( )
2022-10-23 17:44:45 +03:00
if stdout , _ , err := git . NewCommand ( ctx , "remote" , "add" , "origin" ) . AddDynamicArguments ( repoPath ) .
2020-01-12 15:11:17 +03:00
SetDescription ( fmt . Sprintf ( "generateRepoCommit (git remote add): %s to %s" , templateRepoPath , tmpDir ) ) .
2022-04-01 05:55:30 +03:00
RunStdString ( & git . RunOpts { Dir : tmpDir , Env : env } ) ; err != nil {
2020-01-12 15:11:17 +03:00
log . Error ( "Unable to add %v as remote origin to temporary repo to %s: stdout %s\nError: %v" , repo , tmpDir , stdout , err )
2022-10-24 22:29:17 +03:00
return fmt . Errorf ( "git remote add: %w" , err )
2020-01-12 15:11:17 +03:00
}
2022-03-27 05:56:28 +03:00
// set default branch based on whether it's specified in the newly generated repo or not
defaultBranch := repo . DefaultBranch
if strings . TrimSpace ( defaultBranch ) == "" {
defaultBranch = templateRepo . DefaultBranch
}
return initRepoCommit ( ctx , tmpDir , repo , repo . Owner , defaultBranch )
2020-01-12 15:11:17 +03:00
}
2021-12-10 04:27:50 +03:00
func generateGitContent ( ctx context . Context , repo , templateRepo , generateRepo * repo_model . Repository ) ( err error ) {
2021-09-22 08:38:34 +03:00
tmpDir , err := os . MkdirTemp ( os . TempDir ( ) , "gitea-" + repo . Name )
2020-01-12 15:11:17 +03:00
if err != nil {
2022-10-24 22:29:17 +03:00
return fmt . Errorf ( "Failed to create temp dir for repository %s: %w" , repo . RepoPath ( ) , err )
2020-01-12 15:11:17 +03:00
}
defer func ( ) {
2020-08-11 23:05:34 +03:00
if err := util . RemoveAll ( tmpDir ) ; err != nil {
2020-01-12 15:11:17 +03:00
log . Error ( "RemoveAll: %v" , err )
}
} ( )
2022-01-20 02:26:57 +03:00
if err = generateRepoCommit ( ctx , repo , templateRepo , generateRepo , tmpDir ) ; err != nil {
2022-10-24 22:29:17 +03:00
return fmt . Errorf ( "generateRepoCommit: %w" , err )
2020-01-12 15:11:17 +03:00
}
// re-fetch repo
2022-12-03 05:48:26 +03:00
if repo , err = repo_model . GetRepositoryByID ( ctx , repo . ID ) ; err != nil {
2022-10-24 22:29:17 +03:00
return fmt . Errorf ( "getRepositoryByID: %w" , err )
2020-01-12 15:11:17 +03:00
}
2022-03-27 05:56:28 +03:00
// if there was no default branch supplied when generating the repo, use the default one from the template
if strings . TrimSpace ( repo . DefaultBranch ) == "" {
repo . DefaultBranch = templateRepo . DefaultBranch
}
2022-03-29 22:13:41 +03:00
gitRepo , err := git . OpenRepository ( ctx , repo . RepoPath ( ) )
2020-12-12 00:41:59 +03:00
if err != nil {
2022-10-24 22:29:17 +03:00
return fmt . Errorf ( "openRepository: %w" , err )
2020-12-12 00:41:59 +03:00
}
defer gitRepo . Close ( )
if err = gitRepo . SetDefaultBranch ( repo . DefaultBranch ) ; err != nil {
2022-10-24 22:29:17 +03:00
return fmt . Errorf ( "setDefaultBranch: %w" , err )
2020-12-12 00:41:59 +03:00
}
2022-06-06 11:01:49 +03:00
if err = UpdateRepository ( ctx , repo , false ) ; err != nil {
2022-10-24 22:29:17 +03:00
return fmt . Errorf ( "updateRepository: %w" , err )
2020-01-12 15:11:17 +03:00
}
return nil
}
// GenerateGitContent generates git content from a template repository
2021-12-10 04:27:50 +03:00
func GenerateGitContent ( ctx context . Context , templateRepo , generateRepo * repo_model . Repository ) error {
2020-01-12 15:11:17 +03:00
if err := generateGitContent ( ctx , generateRepo , templateRepo , generateRepo ) ; err != nil {
return err
}
2022-06-06 11:01:49 +03:00
if err := UpdateRepoSize ( ctx , generateRepo ) ; err != nil {
2022-10-24 22:29:17 +03:00
return fmt . Errorf ( "failed to update size for repository: %w" , err )
2020-01-12 15:11:17 +03:00
}
2022-06-12 18:51:54 +03:00
if err := git_model . CopyLFS ( ctx , generateRepo , templateRepo ) ; err != nil {
2022-10-24 22:29:17 +03:00
return fmt . Errorf ( "failed to copy LFS: %w" , err )
2020-01-12 15:11:17 +03:00
}
return nil
}
2022-06-06 11:01:49 +03:00
// GenerateRepoOptions contains the template units to generate
type GenerateRepoOptions struct {
Name string
DefaultBranch string
Description string
Private bool
GitContent bool
Topics bool
GitHooks bool
Webhooks bool
Avatar bool
IssueLabels bool
}
// IsValid checks whether at least one option is chosen for generation
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
}
2020-01-12 15:11:17 +03:00
// GenerateRepository generates a repository from a template
2022-06-06 11:01:49 +03:00
func GenerateRepository ( ctx context . Context , doer , owner * user_model . User , templateRepo * repo_model . Repository , opts GenerateRepoOptions ) ( _ * repo_model . Repository , err error ) {
2021-12-10 04:27:50 +03:00
generateRepo := & repo_model . Repository {
2020-01-12 15:11:17 +03:00
OwnerID : owner . ID ,
Owner : owner ,
OwnerName : owner . Name ,
Name : opts . Name ,
LowerName : strings . ToLower ( opts . Name ) ,
Description : opts . Description ,
2022-03-27 05:56:28 +03:00
DefaultBranch : opts . DefaultBranch ,
2020-01-12 15:11:17 +03:00
IsPrivate : opts . Private ,
IsEmpty : ! opts . GitContent || templateRepo . IsEmpty ,
IsFsckEnabled : templateRepo . IsFsckEnabled ,
TemplateID : templateRepo . ID ,
2020-09-19 19:44:55 +03:00
TrustModel : templateRepo . TrustModel ,
2020-01-12 15:11:17 +03:00
}
2023-02-04 09:48:38 +03:00
if err = CreateRepositoryByExample ( ctx , doer , owner , generateRepo , false , false ) ; err != nil {
2020-01-12 15:11:17 +03:00
return nil , err
}
2020-09-25 07:09:23 +03:00
repoPath := generateRepo . RepoPath ( )
2020-11-28 05:42:08 +03:00
isExist , err := util . IsExist ( repoPath )
if err != nil {
log . Error ( "Unable to check if %s exists. Error: %v" , repoPath , err )
return nil , err
}
if isExist {
2021-12-12 18:48:20 +03:00
return nil , repo_model . ErrRepoFilesAlreadyExist {
2020-09-25 07:09:23 +03:00
Uname : generateRepo . OwnerName ,
Name : generateRepo . Name ,
}
}
2022-01-20 02:26:57 +03:00
if err = checkInitRepository ( ctx , owner . Name , generateRepo . Name ) ; err != nil {
2020-01-12 15:11:17 +03:00
return generateRepo , err
}
2022-06-06 11:01:49 +03:00
if err = CheckDaemonExportOK ( ctx , generateRepo ) ; err != nil {
2022-10-24 22:29:17 +03:00
return generateRepo , fmt . Errorf ( "checkDaemonExportOK: %w" , err )
2021-10-13 22:47:02 +03:00
}
2022-04-01 05:55:30 +03:00
if stdout , _ , err := git . NewCommand ( ctx , "update-server-info" ) .
2021-10-13 22:47:02 +03:00
SetDescription ( fmt . Sprintf ( "GenerateRepository(git update-server-info): %s" , repoPath ) ) .
2022-04-01 05:55:30 +03:00
RunStdString ( & git . RunOpts { Dir : repoPath } ) ; err != nil {
2021-10-13 22:47:02 +03:00
log . Error ( "GenerateRepository(git update-server-info) in %v: Stdout: %s\nError: %v" , generateRepo , stdout , err )
2022-10-24 22:29:17 +03:00
return generateRepo , fmt . Errorf ( "error in GenerateRepository(git update-server-info): %w" , err )
2021-10-13 22:47:02 +03:00
}
2020-01-12 15:11:17 +03:00
return generateRepo , nil
}