You can not select more than 25 topics Topics must start with a chinese character,a letter or number, can include dashes ('-') and can be up to 35 characters long.

repo_generate.go 11 kB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390
  1. // Copyright 2019 The Gitea Authors. All rights reserved.
  2. // Use of this source code is governed by a MIT-style
  3. // license that can be found in the LICENSE file.
  4. package models
  5. import (
  6. "fmt"
  7. "io/ioutil"
  8. "os"
  9. "path"
  10. "path/filepath"
  11. "strconv"
  12. "strings"
  13. "time"
  14. "code.gitea.io/gitea/modules/git"
  15. "code.gitea.io/gitea/modules/log"
  16. "code.gitea.io/gitea/modules/process"
  17. "code.gitea.io/gitea/modules/util"
  18. "github.com/gobwas/glob"
  19. "github.com/unknwon/com"
  20. )
  21. // GenerateRepoOptions contains the template units to generate
  22. type GenerateRepoOptions struct {
  23. Name string
  24. Description string
  25. Private bool
  26. GitContent bool
  27. Topics bool
  28. GitHooks bool
  29. Webhooks bool
  30. Avatar bool
  31. IssueLabels bool
  32. }
  33. // IsValid checks whether at least one option is chosen for generation
  34. func (gro GenerateRepoOptions) IsValid() bool {
  35. return gro.GitContent || gro.Topics || gro.GitHooks || gro.Webhooks || gro.Avatar || gro.IssueLabels // or other items as they are added
  36. }
  37. // GiteaTemplate holds information about a .gitea/template file
  38. type GiteaTemplate struct {
  39. Path string
  40. Content []byte
  41. globs []glob.Glob
  42. }
  43. // Globs parses the .gitea/template globs or returns them if they were already parsed
  44. func (gt GiteaTemplate) Globs() []glob.Glob {
  45. if gt.globs != nil {
  46. return gt.globs
  47. }
  48. gt.globs = make([]glob.Glob, 0)
  49. lines := strings.Split(string(util.NormalizeEOL(gt.Content)), "\n")
  50. for _, line := range lines {
  51. line = strings.TrimSpace(line)
  52. if line == "" || strings.HasPrefix(line, "#") {
  53. continue
  54. }
  55. g, err := glob.Compile(line, '/')
  56. if err != nil {
  57. log.Info("Invalid glob expression '%s' (skipped): %v", line, err)
  58. continue
  59. }
  60. gt.globs = append(gt.globs, g)
  61. }
  62. return gt.globs
  63. }
  64. func checkGiteaTemplate(tmpDir string) (*GiteaTemplate, error) {
  65. gtPath := filepath.Join(tmpDir, ".gitea", "template")
  66. if _, err := os.Stat(gtPath); os.IsNotExist(err) {
  67. return nil, nil
  68. } else if err != nil {
  69. return nil, err
  70. }
  71. content, err := ioutil.ReadFile(gtPath)
  72. if err != nil {
  73. return nil, err
  74. }
  75. gt := &GiteaTemplate{
  76. Path: gtPath,
  77. Content: content,
  78. }
  79. return gt, nil
  80. }
  81. func generateRepoCommit(e Engine, repo, templateRepo, generateRepo *Repository, tmpDir string) error {
  82. commitTimeStr := time.Now().Format(time.RFC3339)
  83. authorSig := repo.Owner.NewGitSig()
  84. // Because this may call hooks we should pass in the environment
  85. env := append(os.Environ(),
  86. "GIT_AUTHOR_NAME="+authorSig.Name,
  87. "GIT_AUTHOR_EMAIL="+authorSig.Email,
  88. "GIT_AUTHOR_DATE="+commitTimeStr,
  89. "GIT_COMMITTER_NAME="+authorSig.Name,
  90. "GIT_COMMITTER_EMAIL="+authorSig.Email,
  91. "GIT_COMMITTER_DATE="+commitTimeStr,
  92. )
  93. // Clone to temporary path and do the init commit.
  94. templateRepoPath := templateRepo.repoPath(e)
  95. if err := git.Clone(templateRepoPath, tmpDir, git.CloneRepoOptions{
  96. Depth: 1,
  97. }); err != nil {
  98. return fmt.Errorf("git clone: %v", err)
  99. }
  100. if err := os.RemoveAll(path.Join(tmpDir, ".git")); err != nil {
  101. return fmt.Errorf("remove git dir: %v", err)
  102. }
  103. // Variable expansion
  104. gt, err := checkGiteaTemplate(tmpDir)
  105. if err != nil {
  106. return fmt.Errorf("checkGiteaTemplate: %v", err)
  107. }
  108. if err := os.Remove(gt.Path); err != nil {
  109. return fmt.Errorf("remove .giteatemplate: %v", err)
  110. }
  111. // Avoid walking tree if there are no globs
  112. if len(gt.Globs()) > 0 {
  113. tmpDirSlash := strings.TrimSuffix(filepath.ToSlash(tmpDir), "/") + "/"
  114. if err := filepath.Walk(tmpDirSlash, func(path string, info os.FileInfo, walkErr error) error {
  115. if walkErr != nil {
  116. return walkErr
  117. }
  118. if info.IsDir() {
  119. return nil
  120. }
  121. base := strings.TrimPrefix(filepath.ToSlash(path), tmpDirSlash)
  122. for _, g := range gt.Globs() {
  123. if g.Match(base) {
  124. content, err := ioutil.ReadFile(path)
  125. if err != nil {
  126. return err
  127. }
  128. if err := ioutil.WriteFile(path,
  129. []byte(generateExpansion(string(content), templateRepo, generateRepo)),
  130. 0644); err != nil {
  131. return err
  132. }
  133. break
  134. }
  135. }
  136. return nil
  137. }); err != nil {
  138. return err
  139. }
  140. }
  141. if err := git.InitRepository(tmpDir, false); err != nil {
  142. return err
  143. }
  144. repoPath := repo.repoPath(e)
  145. _, stderr, err := process.GetManager().ExecDirEnv(
  146. -1, tmpDir,
  147. fmt.Sprintf("generateRepoCommit(git remote add): %s", repoPath),
  148. env,
  149. git.GitExecutable, "remote", "add", "origin", repoPath,
  150. )
  151. if err != nil {
  152. return fmt.Errorf("git remote add: %v - %s", err, stderr)
  153. }
  154. return initRepoCommit(tmpDir, repo.Owner)
  155. }
  156. // generateRepository initializes repository from template
  157. func generateRepository(e Engine, repo, templateRepo, generateRepo *Repository) (err error) {
  158. tmpDir := filepath.Join(os.TempDir(), "gitea-"+repo.Name+"-"+com.ToStr(time.Now().Nanosecond()))
  159. if err := os.MkdirAll(tmpDir, os.ModePerm); err != nil {
  160. return fmt.Errorf("Failed to create dir %s: %v", tmpDir, err)
  161. }
  162. defer func() {
  163. if err := os.RemoveAll(tmpDir); err != nil {
  164. log.Error("RemoveAll: %v", err)
  165. }
  166. }()
  167. if err = generateRepoCommit(e, repo, templateRepo, generateRepo, tmpDir); err != nil {
  168. return fmt.Errorf("generateRepoCommit: %v", err)
  169. }
  170. // re-fetch repo
  171. if repo, err = getRepositoryByID(e, repo.ID); err != nil {
  172. return fmt.Errorf("getRepositoryByID: %v", err)
  173. }
  174. repo.DefaultBranch = "master"
  175. if err = updateRepository(e, repo, false); err != nil {
  176. return fmt.Errorf("updateRepository: %v", err)
  177. }
  178. return nil
  179. }
  180. // GenerateRepository generates a repository from a template
  181. func GenerateRepository(ctx DBContext, doer, owner *User, templateRepo *Repository, opts GenerateRepoOptions) (_ *Repository, err error) {
  182. generateRepo := &Repository{
  183. OwnerID: owner.ID,
  184. Owner: owner,
  185. Name: opts.Name,
  186. LowerName: strings.ToLower(opts.Name),
  187. Description: opts.Description,
  188. IsPrivate: opts.Private,
  189. IsEmpty: !opts.GitContent || templateRepo.IsEmpty,
  190. IsFsckEnabled: templateRepo.IsFsckEnabled,
  191. TemplateID: templateRepo.ID,
  192. }
  193. if err = createRepository(ctx.e, doer, owner, generateRepo); err != nil {
  194. return nil, err
  195. }
  196. repoPath := RepoPath(owner.Name, generateRepo.Name)
  197. if err = checkInitRepository(repoPath); err != nil {
  198. return generateRepo, err
  199. }
  200. return generateRepo, nil
  201. }
  202. // GenerateGitContent generates git content from a template repository
  203. func GenerateGitContent(ctx DBContext, templateRepo, generateRepo *Repository) error {
  204. if err := generateRepository(ctx.e, generateRepo, templateRepo, generateRepo); err != nil {
  205. return err
  206. }
  207. if err := generateRepo.updateSize(ctx.e); err != nil {
  208. return fmt.Errorf("failed to update size for repository: %v", err)
  209. }
  210. if err := copyLFS(ctx.e, generateRepo, templateRepo); err != nil {
  211. return fmt.Errorf("failed to copy LFS: %v", err)
  212. }
  213. return nil
  214. }
  215. // GenerateTopics generates topics from a template repository
  216. func GenerateTopics(ctx DBContext, templateRepo, generateRepo *Repository) error {
  217. for _, topic := range templateRepo.Topics {
  218. if _, err := addTopicByNameToRepo(ctx.e, generateRepo.ID, topic); err != nil {
  219. return err
  220. }
  221. }
  222. return nil
  223. }
  224. // GenerateGitHooks generates git hooks from a template repository
  225. func GenerateGitHooks(ctx DBContext, templateRepo, generateRepo *Repository) error {
  226. generateGitRepo, err := git.OpenRepository(generateRepo.repoPath(ctx.e))
  227. if err != nil {
  228. return err
  229. }
  230. defer generateGitRepo.Close()
  231. templateGitRepo, err := git.OpenRepository(templateRepo.repoPath(ctx.e))
  232. if err != nil {
  233. return err
  234. }
  235. defer templateGitRepo.Close()
  236. templateHooks, err := templateGitRepo.Hooks()
  237. if err != nil {
  238. return err
  239. }
  240. for _, templateHook := range templateHooks {
  241. generateHook, err := generateGitRepo.GetHook(templateHook.Name())
  242. if err != nil {
  243. return err
  244. }
  245. generateHook.Content = templateHook.Content
  246. if err := generateHook.Update(); err != nil {
  247. return err
  248. }
  249. }
  250. return nil
  251. }
  252. // GenerateWebhooks generates webhooks from a template repository
  253. func GenerateWebhooks(ctx DBContext, templateRepo, generateRepo *Repository) error {
  254. templateWebhooks, err := GetWebhooksByRepoID(templateRepo.ID)
  255. if err != nil {
  256. return err
  257. }
  258. for _, templateWebhook := range templateWebhooks {
  259. generateWebhook := &Webhook{
  260. RepoID: generateRepo.ID,
  261. URL: templateWebhook.URL,
  262. HTTPMethod: templateWebhook.HTTPMethod,
  263. ContentType: templateWebhook.ContentType,
  264. Secret: templateWebhook.Secret,
  265. HookEvent: templateWebhook.HookEvent,
  266. IsActive: templateWebhook.IsActive,
  267. HookTaskType: templateWebhook.HookTaskType,
  268. OrgID: templateWebhook.OrgID,
  269. Events: templateWebhook.Events,
  270. Meta: templateWebhook.Meta,
  271. }
  272. if err := createWebhook(ctx.e, generateWebhook); err != nil {
  273. return err
  274. }
  275. }
  276. return nil
  277. }
  278. // GenerateAvatar generates the avatar from a template repository
  279. func GenerateAvatar(ctx DBContext, templateRepo, generateRepo *Repository) error {
  280. generateRepo.Avatar = strings.Replace(templateRepo.Avatar, strconv.FormatInt(templateRepo.ID, 10), strconv.FormatInt(generateRepo.ID, 10), 1)
  281. if err := com.Copy(templateRepo.CustomAvatarPath(), generateRepo.CustomAvatarPath()); err != nil {
  282. return err
  283. }
  284. return updateRepositoryCols(ctx.e, generateRepo, "avatar")
  285. }
  286. // GenerateIssueLabels generates issue labels from a template repository
  287. func GenerateIssueLabels(ctx DBContext, templateRepo, generateRepo *Repository) error {
  288. templateLabels, err := getLabelsByRepoID(ctx.e, templateRepo.ID, "")
  289. if err != nil {
  290. return err
  291. }
  292. for _, templateLabel := range templateLabels {
  293. generateLabel := &Label{
  294. RepoID: generateRepo.ID,
  295. Name: templateLabel.Name,
  296. Description: templateLabel.Description,
  297. Color: templateLabel.Color,
  298. }
  299. if err := newLabel(ctx.e, generateLabel); err != nil {
  300. return err
  301. }
  302. }
  303. return nil
  304. }
  305. func generateExpansion(src string, templateRepo, generateRepo *Repository) string {
  306. return os.Expand(src, func(key string) string {
  307. switch key {
  308. case "REPO_NAME":
  309. return generateRepo.Name
  310. case "TEMPLATE_NAME":
  311. return templateRepo.Name
  312. case "REPO_DESCRIPTION":
  313. return generateRepo.Description
  314. case "TEMPLATE_DESCRIPTION":
  315. return templateRepo.Description
  316. case "REPO_OWNER":
  317. return generateRepo.MustOwnerName()
  318. case "TEMPLATE_OWNER":
  319. return templateRepo.MustOwnerName()
  320. case "REPO_LINK":
  321. return generateRepo.Link()
  322. case "TEMPLATE_LINK":
  323. return templateRepo.Link()
  324. case "REPO_HTTPS_URL":
  325. return generateRepo.CloneLink().HTTPS
  326. case "TEMPLATE_HTTPS_URL":
  327. return templateRepo.CloneLink().HTTPS
  328. case "REPO_SSH_URL":
  329. return generateRepo.CloneLink().SSH
  330. case "TEMPLATE_SSH_URL":
  331. return templateRepo.CloneLink().SSH
  332. default:
  333. return key
  334. }
  335. })
  336. }