gitea源码

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326
  1. // Copyright 2019 The Gitea Authors. All rights reserved.
  2. // SPDX-License-Identifier: MIT
  3. package repository
  4. import (
  5. "bufio"
  6. "bytes"
  7. "context"
  8. "fmt"
  9. "os"
  10. "path/filepath"
  11. "regexp"
  12. "strconv"
  13. "strings"
  14. "time"
  15. git_model "code.gitea.io/gitea/models/git"
  16. repo_model "code.gitea.io/gitea/models/repo"
  17. "code.gitea.io/gitea/modules/git"
  18. "code.gitea.io/gitea/modules/git/gitcmd"
  19. "code.gitea.io/gitea/modules/gitrepo"
  20. "code.gitea.io/gitea/modules/glob"
  21. "code.gitea.io/gitea/modules/log"
  22. repo_module "code.gitea.io/gitea/modules/repository"
  23. "code.gitea.io/gitea/modules/setting"
  24. "code.gitea.io/gitea/modules/util"
  25. "github.com/huandu/xstrings"
  26. )
  27. type transformer struct {
  28. Name string
  29. Transform func(string) string
  30. }
  31. type expansion struct {
  32. Name string
  33. Value string
  34. Transformers []transformer
  35. }
  36. var defaultTransformers = []transformer{
  37. {Name: "SNAKE", Transform: xstrings.ToSnakeCase},
  38. {Name: "KEBAB", Transform: xstrings.ToKebabCase},
  39. {Name: "CAMEL", Transform: xstrings.ToCamelCase},
  40. {Name: "PASCAL", Transform: xstrings.ToPascalCase},
  41. {Name: "LOWER", Transform: strings.ToLower},
  42. {Name: "UPPER", Transform: strings.ToUpper},
  43. {Name: "TITLE", Transform: util.ToTitleCase},
  44. }
  45. func generateExpansion(ctx context.Context, src string, templateRepo, generateRepo *repo_model.Repository, sanitizeFileName bool) string {
  46. year, month, day := time.Now().Date()
  47. expansions := []expansion{
  48. {Name: "YEAR", Value: strconv.Itoa(year), Transformers: nil},
  49. {Name: "MONTH", Value: fmt.Sprintf("%02d", int(month)), Transformers: nil},
  50. {Name: "MONTH_ENGLISH", Value: month.String(), Transformers: defaultTransformers},
  51. {Name: "DAY", Value: fmt.Sprintf("%02d", day), Transformers: nil},
  52. {Name: "REPO_NAME", Value: generateRepo.Name, Transformers: defaultTransformers},
  53. {Name: "TEMPLATE_NAME", Value: templateRepo.Name, Transformers: defaultTransformers},
  54. {Name: "REPO_DESCRIPTION", Value: generateRepo.Description, Transformers: nil},
  55. {Name: "TEMPLATE_DESCRIPTION", Value: templateRepo.Description, Transformers: nil},
  56. {Name: "REPO_OWNER", Value: generateRepo.OwnerName, Transformers: defaultTransformers},
  57. {Name: "TEMPLATE_OWNER", Value: templateRepo.OwnerName, Transformers: defaultTransformers},
  58. {Name: "REPO_LINK", Value: generateRepo.Link(), Transformers: nil},
  59. {Name: "TEMPLATE_LINK", Value: templateRepo.Link(), Transformers: nil},
  60. {Name: "REPO_HTTPS_URL", Value: generateRepo.CloneLinkGeneral(ctx).HTTPS, Transformers: nil},
  61. {Name: "TEMPLATE_HTTPS_URL", Value: templateRepo.CloneLinkGeneral(ctx).HTTPS, Transformers: nil},
  62. {Name: "REPO_SSH_URL", Value: generateRepo.CloneLinkGeneral(ctx).SSH, Transformers: nil},
  63. {Name: "TEMPLATE_SSH_URL", Value: templateRepo.CloneLinkGeneral(ctx).SSH, Transformers: nil},
  64. }
  65. expansionMap := make(map[string]string)
  66. for _, e := range expansions {
  67. expansionMap[e.Name] = e.Value
  68. for _, tr := range e.Transformers {
  69. expansionMap[fmt.Sprintf("%s_%s", e.Name, tr.Name)] = tr.Transform(e.Value)
  70. }
  71. }
  72. return os.Expand(src, func(key string) string {
  73. if expansion, ok := expansionMap[key]; ok {
  74. if sanitizeFileName {
  75. return fileNameSanitize(expansion)
  76. }
  77. return expansion
  78. }
  79. return key
  80. })
  81. }
  82. // GiteaTemplate holds information about a .gitea/template file
  83. type GiteaTemplate struct {
  84. Path string
  85. Content []byte
  86. globs []glob.Glob
  87. }
  88. // Globs parses the .gitea/template globs or returns them if they were already parsed
  89. func (gt *GiteaTemplate) Globs() []glob.Glob {
  90. if gt.globs != nil {
  91. return gt.globs
  92. }
  93. gt.globs = make([]glob.Glob, 0)
  94. scanner := bufio.NewScanner(bytes.NewReader(gt.Content))
  95. for scanner.Scan() {
  96. line := strings.TrimSpace(scanner.Text())
  97. if line == "" || strings.HasPrefix(line, "#") {
  98. continue
  99. }
  100. g, err := glob.Compile(line, '/')
  101. if err != nil {
  102. log.Info("Invalid glob expression '%s' (skipped): %v", line, err)
  103. continue
  104. }
  105. gt.globs = append(gt.globs, g)
  106. }
  107. return gt.globs
  108. }
  109. func readGiteaTemplateFile(tmpDir string) (*GiteaTemplate, error) {
  110. gtPath := filepath.Join(tmpDir, ".gitea", "template")
  111. if _, err := os.Stat(gtPath); os.IsNotExist(err) {
  112. return nil, nil
  113. } else if err != nil {
  114. return nil, err
  115. }
  116. content, err := os.ReadFile(gtPath)
  117. if err != nil {
  118. return nil, err
  119. }
  120. return &GiteaTemplate{Path: gtPath, Content: content}, nil
  121. }
  122. func processGiteaTemplateFile(ctx context.Context, tmpDir string, templateRepo, generateRepo *repo_model.Repository, giteaTemplateFile *GiteaTemplate) error {
  123. if err := util.Remove(giteaTemplateFile.Path); err != nil {
  124. return fmt.Errorf("remove .giteatemplate: %w", err)
  125. }
  126. if len(giteaTemplateFile.Globs()) == 0 {
  127. return nil // Avoid walking tree if there are no globs
  128. }
  129. tmpDirSlash := strings.TrimSuffix(filepath.ToSlash(tmpDir), "/") + "/"
  130. return filepath.WalkDir(tmpDirSlash, func(path string, d os.DirEntry, walkErr error) error {
  131. if walkErr != nil {
  132. return walkErr
  133. }
  134. if d.IsDir() {
  135. return nil
  136. }
  137. base := strings.TrimPrefix(filepath.ToSlash(path), tmpDirSlash)
  138. for _, g := range giteaTemplateFile.Globs() {
  139. if g.Match(base) {
  140. content, err := os.ReadFile(path)
  141. if err != nil {
  142. return err
  143. }
  144. generatedContent := []byte(generateExpansion(ctx, string(content), templateRepo, generateRepo, false))
  145. if err := os.WriteFile(path, generatedContent, 0o644); err != nil {
  146. return err
  147. }
  148. substPath := filepath.FromSlash(filepath.Join(tmpDirSlash, generateExpansion(ctx, base, templateRepo, generateRepo, true)))
  149. // Create parent subdirectories if needed or continue silently if it exists
  150. if err = os.MkdirAll(filepath.Dir(substPath), 0o755); err != nil {
  151. return err
  152. }
  153. // Substitute filename variables
  154. if err = os.Rename(path, substPath); err != nil {
  155. return err
  156. }
  157. break
  158. }
  159. }
  160. return nil
  161. }) // end: WalkDir
  162. }
  163. func generateRepoCommit(ctx context.Context, repo, templateRepo, generateRepo *repo_model.Repository, tmpDir string) error {
  164. commitTimeStr := time.Now().Format(time.RFC3339)
  165. authorSig := repo.Owner.NewGitSig()
  166. // Because this may call hooks we should pass in the environment
  167. env := append(os.Environ(),
  168. "GIT_AUTHOR_NAME="+authorSig.Name,
  169. "GIT_AUTHOR_EMAIL="+authorSig.Email,
  170. "GIT_AUTHOR_DATE="+commitTimeStr,
  171. "GIT_COMMITTER_NAME="+authorSig.Name,
  172. "GIT_COMMITTER_EMAIL="+authorSig.Email,
  173. "GIT_COMMITTER_DATE="+commitTimeStr,
  174. )
  175. // Clone to temporary path and do the init commit.
  176. templateRepoPath := templateRepo.RepoPath()
  177. if err := git.Clone(ctx, templateRepoPath, tmpDir, git.CloneRepoOptions{
  178. Depth: 1,
  179. Branch: templateRepo.DefaultBranch,
  180. }); err != nil {
  181. return fmt.Errorf("git clone: %w", err)
  182. }
  183. // Get active submodules from the template
  184. submodules, err := git.GetTemplateSubmoduleCommits(ctx, tmpDir)
  185. if err != nil {
  186. return fmt.Errorf("GetTemplateSubmoduleCommits: %w", err)
  187. }
  188. if err = util.RemoveAll(filepath.Join(tmpDir, ".git")); err != nil {
  189. return fmt.Errorf("remove git dir: %w", err)
  190. }
  191. // Variable expansion
  192. giteaTemplateFile, err := readGiteaTemplateFile(tmpDir)
  193. if err != nil {
  194. return fmt.Errorf("readGiteaTemplateFile: %w", err)
  195. }
  196. if giteaTemplateFile != nil {
  197. err = processGiteaTemplateFile(ctx, tmpDir, templateRepo, generateRepo, giteaTemplateFile)
  198. if err != nil {
  199. return err
  200. }
  201. }
  202. if err = git.InitRepository(ctx, tmpDir, false, templateRepo.ObjectFormatName); err != nil {
  203. return err
  204. }
  205. if stdout, _, err := gitcmd.NewCommand("remote", "add", "origin").AddDynamicArguments(repo.RepoPath()).
  206. RunStdString(ctx, &gitcmd.RunOpts{Dir: tmpDir, Env: env}); err != nil {
  207. log.Error("Unable to add %v as remote origin to temporary repo to %s: stdout %s\nError: %v", repo, tmpDir, stdout, err)
  208. return fmt.Errorf("git remote add: %w", err)
  209. }
  210. if err = git.AddTemplateSubmoduleIndexes(ctx, tmpDir, submodules); err != nil {
  211. return fmt.Errorf("failed to add submodules: %v", err)
  212. }
  213. // set default branch based on whether it's specified in the newly generated repo or not
  214. defaultBranch := repo.DefaultBranch
  215. if strings.TrimSpace(defaultBranch) == "" {
  216. defaultBranch = templateRepo.DefaultBranch
  217. }
  218. return initRepoCommit(ctx, tmpDir, repo, repo.Owner, defaultBranch)
  219. }
  220. // GenerateGitContent generates git content from a template repository
  221. func GenerateGitContent(ctx context.Context, templateRepo, generateRepo *repo_model.Repository) (err error) {
  222. tmpDir, cleanup, err := setting.AppDataTempDir("git-repo-content").MkdirTempRandom("gitea-" + generateRepo.Name)
  223. if err != nil {
  224. return fmt.Errorf("failed to create temp dir for repository %s: %w", generateRepo.FullName(), err)
  225. }
  226. defer cleanup()
  227. if err = generateRepoCommit(ctx, generateRepo, templateRepo, generateRepo, tmpDir); err != nil {
  228. return fmt.Errorf("generateRepoCommit: %w", err)
  229. }
  230. // re-fetch repo
  231. if generateRepo, err = repo_model.GetRepositoryByID(ctx, generateRepo.ID); err != nil {
  232. return fmt.Errorf("getRepositoryByID: %w", err)
  233. }
  234. // if there was no default branch supplied when generating the repo, use the default one from the template
  235. if strings.TrimSpace(generateRepo.DefaultBranch) == "" {
  236. generateRepo.DefaultBranch = templateRepo.DefaultBranch
  237. }
  238. if err = gitrepo.SetDefaultBranch(ctx, generateRepo, generateRepo.DefaultBranch); err != nil {
  239. return fmt.Errorf("setDefaultBranch: %w", err)
  240. }
  241. if err = repo_model.UpdateRepositoryColsNoAutoTime(ctx, generateRepo, "default_branch"); err != nil {
  242. return fmt.Errorf("updateRepository: %w", err)
  243. }
  244. if err := repo_module.UpdateRepoSize(ctx, generateRepo); err != nil {
  245. return fmt.Errorf("failed to update size for repository: %w", err)
  246. }
  247. if err := git_model.CopyLFS(ctx, generateRepo, templateRepo); err != nil {
  248. return fmt.Errorf("failed to copy LFS: %w", err)
  249. }
  250. return nil
  251. }
  252. // GenerateRepoOptions contains the template units to generate
  253. type GenerateRepoOptions struct {
  254. Name string
  255. DefaultBranch string
  256. Description string
  257. Private bool
  258. GitContent bool
  259. Topics bool
  260. GitHooks bool
  261. Webhooks bool
  262. Avatar bool
  263. IssueLabels bool
  264. ProtectedBranch bool
  265. }
  266. // IsValid checks whether at least one option is chosen for generation
  267. func (gro GenerateRepoOptions) IsValid() bool {
  268. return gro.GitContent || gro.Topics || gro.GitHooks || gro.Webhooks || gro.Avatar ||
  269. gro.IssueLabels || gro.ProtectedBranch // or other items as they are added
  270. }
  271. var fileNameSanitizeRegexp = regexp.MustCompile(`(?i)\.\.|[<>:\"/\\|?*\x{0000}-\x{001F}]|^(con|prn|aux|nul|com\d|lpt\d)$`)
  272. // Sanitize user input to valid OS filenames
  273. //
  274. // Based on https://github.com/sindresorhus/filename-reserved-regex
  275. // Adds ".." to prevent directory traversal
  276. func fileNameSanitize(s string) string {
  277. return strings.TrimSpace(fileNameSanitizeRegexp.ReplaceAllString(s, "_"))
  278. }