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

create.go 10KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318
  1. // Copyright 2023 The Gitea Authors. All rights reserved.
  2. // SPDX-License-Identifier: MIT
  3. package repository
  4. import (
  5. "bytes"
  6. "context"
  7. "fmt"
  8. "os"
  9. "path/filepath"
  10. "strings"
  11. "time"
  12. "code.gitea.io/gitea/models/db"
  13. repo_model "code.gitea.io/gitea/models/repo"
  14. user_model "code.gitea.io/gitea/models/user"
  15. "code.gitea.io/gitea/modules/git"
  16. "code.gitea.io/gitea/modules/gitrepo"
  17. "code.gitea.io/gitea/modules/log"
  18. "code.gitea.io/gitea/modules/options"
  19. repo_module "code.gitea.io/gitea/modules/repository"
  20. "code.gitea.io/gitea/modules/setting"
  21. api "code.gitea.io/gitea/modules/structs"
  22. "code.gitea.io/gitea/modules/templates/vars"
  23. "code.gitea.io/gitea/modules/util"
  24. )
  25. // CreateRepoOptions contains the create repository options
  26. type CreateRepoOptions struct {
  27. Name string
  28. Description string
  29. OriginalURL string
  30. GitServiceType api.GitServiceType
  31. Gitignores string
  32. IssueLabels string
  33. License string
  34. Readme string
  35. DefaultBranch string
  36. IsPrivate bool
  37. IsMirror bool
  38. IsTemplate bool
  39. AutoInit bool
  40. Status repo_model.RepositoryStatus
  41. TrustModel repo_model.TrustModelType
  42. MirrorInterval string
  43. ObjectFormatName string
  44. }
  45. func prepareRepoCommit(ctx context.Context, repo *repo_model.Repository, tmpDir, repoPath string, opts CreateRepoOptions) error {
  46. commitTimeStr := time.Now().Format(time.RFC3339)
  47. authorSig := repo.Owner.NewGitSig()
  48. // Because this may call hooks we should pass in the environment
  49. env := append(os.Environ(),
  50. "GIT_AUTHOR_NAME="+authorSig.Name,
  51. "GIT_AUTHOR_EMAIL="+authorSig.Email,
  52. "GIT_AUTHOR_DATE="+commitTimeStr,
  53. "GIT_COMMITTER_NAME="+authorSig.Name,
  54. "GIT_COMMITTER_EMAIL="+authorSig.Email,
  55. "GIT_COMMITTER_DATE="+commitTimeStr,
  56. )
  57. // Clone to temporary path and do the init commit.
  58. if stdout, _, err := git.NewCommand(ctx, "clone").AddDynamicArguments(repoPath, tmpDir).
  59. SetDescription(fmt.Sprintf("prepareRepoCommit (git clone): %s to %s", repoPath, tmpDir)).
  60. RunStdString(&git.RunOpts{Dir: "", Env: env}); err != nil {
  61. log.Error("Failed to clone from %v into %s: stdout: %s\nError: %v", repo, tmpDir, stdout, err)
  62. return fmt.Errorf("git clone: %w", err)
  63. }
  64. // README
  65. data, err := options.Readme(opts.Readme)
  66. if err != nil {
  67. return fmt.Errorf("GetRepoInitFile[%s]: %w", opts.Readme, err)
  68. }
  69. cloneLink := repo.CloneLink()
  70. match := map[string]string{
  71. "Name": repo.Name,
  72. "Description": repo.Description,
  73. "CloneURL.SSH": cloneLink.SSH,
  74. "CloneURL.HTTPS": cloneLink.HTTPS,
  75. "OwnerName": repo.OwnerName,
  76. }
  77. res, err := vars.Expand(string(data), match)
  78. if err != nil {
  79. // here we could just log the error and continue the rendering
  80. log.Error("unable to expand template vars for repo README: %s, err: %v", opts.Readme, err)
  81. }
  82. if err = os.WriteFile(filepath.Join(tmpDir, "README.md"),
  83. []byte(res), 0o644); err != nil {
  84. return fmt.Errorf("write README.md: %w", err)
  85. }
  86. // .gitignore
  87. if len(opts.Gitignores) > 0 {
  88. var buf bytes.Buffer
  89. names := strings.Split(opts.Gitignores, ",")
  90. for _, name := range names {
  91. data, err = options.Gitignore(name)
  92. if err != nil {
  93. return fmt.Errorf("GetRepoInitFile[%s]: %w", name, err)
  94. }
  95. buf.WriteString("# ---> " + name + "\n")
  96. buf.Write(data)
  97. buf.WriteString("\n")
  98. }
  99. if buf.Len() > 0 {
  100. if err = os.WriteFile(filepath.Join(tmpDir, ".gitignore"), buf.Bytes(), 0o644); err != nil {
  101. return fmt.Errorf("write .gitignore: %w", err)
  102. }
  103. }
  104. }
  105. // LICENSE
  106. if len(opts.License) > 0 {
  107. data, err = repo_module.GetLicense(opts.License, &repo_module.LicenseValues{
  108. Owner: repo.OwnerName,
  109. Email: authorSig.Email,
  110. Repo: repo.Name,
  111. Year: time.Now().Format("2006"),
  112. })
  113. if err != nil {
  114. return fmt.Errorf("getLicense[%s]: %w", opts.License, err)
  115. }
  116. if err = os.WriteFile(filepath.Join(tmpDir, "LICENSE"), data, 0o644); err != nil {
  117. return fmt.Errorf("write LICENSE: %w", err)
  118. }
  119. }
  120. return nil
  121. }
  122. // InitRepository initializes README and .gitignore if needed.
  123. func initRepository(ctx context.Context, repoPath string, u *user_model.User, repo *repo_model.Repository, opts CreateRepoOptions) (err error) {
  124. if err = repo_module.CheckInitRepository(ctx, repo.OwnerName, repo.Name, opts.ObjectFormatName); err != nil {
  125. return err
  126. }
  127. // Initialize repository according to user's choice.
  128. if opts.AutoInit {
  129. tmpDir, err := os.MkdirTemp(os.TempDir(), "gitea-"+repo.Name)
  130. if err != nil {
  131. return fmt.Errorf("Failed to create temp dir for repository %s: %w", repo.RepoPath(), err)
  132. }
  133. defer func() {
  134. if err := util.RemoveAll(tmpDir); err != nil {
  135. log.Warn("Unable to remove temporary directory: %s: Error: %v", tmpDir, err)
  136. }
  137. }()
  138. if err = prepareRepoCommit(ctx, repo, tmpDir, repoPath, opts); err != nil {
  139. return fmt.Errorf("prepareRepoCommit: %w", err)
  140. }
  141. // Apply changes and commit.
  142. if err = initRepoCommit(ctx, tmpDir, repo, u, opts.DefaultBranch); err != nil {
  143. return fmt.Errorf("initRepoCommit: %w", err)
  144. }
  145. }
  146. // Re-fetch the repository from database before updating it (else it would
  147. // override changes that were done earlier with sql)
  148. if repo, err = repo_model.GetRepositoryByID(ctx, repo.ID); err != nil {
  149. return fmt.Errorf("getRepositoryByID: %w", err)
  150. }
  151. if !opts.AutoInit {
  152. repo.IsEmpty = true
  153. }
  154. repo.DefaultBranch = setting.Repository.DefaultBranch
  155. repo.DefaultWikiBranch = setting.Repository.DefaultBranch
  156. if len(opts.DefaultBranch) > 0 {
  157. repo.DefaultBranch = opts.DefaultBranch
  158. if err = gitrepo.SetDefaultBranch(ctx, repo, repo.DefaultBranch); err != nil {
  159. return fmt.Errorf("setDefaultBranch: %w", err)
  160. }
  161. if !repo.IsEmpty {
  162. if _, err := repo_module.SyncRepoBranches(ctx, repo.ID, u.ID); err != nil {
  163. return fmt.Errorf("SyncRepoBranches: %w", err)
  164. }
  165. }
  166. }
  167. if err = UpdateRepository(ctx, repo, false); err != nil {
  168. return fmt.Errorf("updateRepository: %w", err)
  169. }
  170. return nil
  171. }
  172. // CreateRepositoryDirectly creates a repository for the user/organization.
  173. func CreateRepositoryDirectly(ctx context.Context, doer, u *user_model.User, opts CreateRepoOptions) (*repo_model.Repository, error) {
  174. if !doer.IsAdmin && !u.CanCreateRepo() {
  175. return nil, repo_model.ErrReachLimitOfRepo{
  176. Limit: u.MaxRepoCreation,
  177. }
  178. }
  179. if len(opts.DefaultBranch) == 0 {
  180. opts.DefaultBranch = setting.Repository.DefaultBranch
  181. }
  182. // Check if label template exist
  183. if len(opts.IssueLabels) > 0 {
  184. if _, err := repo_module.LoadTemplateLabelsByDisplayName(opts.IssueLabels); err != nil {
  185. return nil, err
  186. }
  187. }
  188. if opts.ObjectFormatName == "" {
  189. opts.ObjectFormatName = git.Sha1ObjectFormat.Name()
  190. }
  191. repo := &repo_model.Repository{
  192. OwnerID: u.ID,
  193. Owner: u,
  194. OwnerName: u.Name,
  195. Name: opts.Name,
  196. LowerName: strings.ToLower(opts.Name),
  197. Description: opts.Description,
  198. OriginalURL: opts.OriginalURL,
  199. OriginalServiceType: opts.GitServiceType,
  200. IsPrivate: opts.IsPrivate,
  201. IsFsckEnabled: !opts.IsMirror,
  202. IsTemplate: opts.IsTemplate,
  203. CloseIssuesViaCommitInAnyBranch: setting.Repository.DefaultCloseIssuesViaCommitsInAnyBranch,
  204. Status: opts.Status,
  205. IsEmpty: !opts.AutoInit,
  206. TrustModel: opts.TrustModel,
  207. IsMirror: opts.IsMirror,
  208. DefaultBranch: opts.DefaultBranch,
  209. DefaultWikiBranch: setting.Repository.DefaultBranch,
  210. ObjectFormatName: opts.ObjectFormatName,
  211. }
  212. var rollbackRepo *repo_model.Repository
  213. if err := db.WithTx(ctx, func(ctx context.Context) error {
  214. if err := repo_module.CreateRepositoryByExample(ctx, doer, u, repo, false, false); err != nil {
  215. return err
  216. }
  217. // No need for init mirror.
  218. if opts.IsMirror {
  219. return nil
  220. }
  221. repoPath := repo_model.RepoPath(u.Name, repo.Name)
  222. isExist, err := util.IsExist(repoPath)
  223. if err != nil {
  224. log.Error("Unable to check if %s exists. Error: %v", repoPath, err)
  225. return err
  226. }
  227. if isExist {
  228. // repo already exists - We have two or three options.
  229. // 1. We fail stating that the directory exists
  230. // 2. We create the db repository to go with this data and adopt the git repo
  231. // 3. We delete it and start afresh
  232. //
  233. // Previously Gitea would just delete and start afresh - this was naughty.
  234. // So we will now fail and delegate to other functionality to adopt or delete
  235. log.Error("Files already exist in %s and we are not going to adopt or delete.", repoPath)
  236. return repo_model.ErrRepoFilesAlreadyExist{
  237. Uname: u.Name,
  238. Name: repo.Name,
  239. }
  240. }
  241. if err = initRepository(ctx, repoPath, doer, repo, opts); err != nil {
  242. if err2 := util.RemoveAll(repoPath); err2 != nil {
  243. log.Error("initRepository: %v", err)
  244. return fmt.Errorf(
  245. "delete repo directory %s/%s failed(2): %v", u.Name, repo.Name, err2)
  246. }
  247. return fmt.Errorf("initRepository: %w", err)
  248. }
  249. // Initialize Issue Labels if selected
  250. if len(opts.IssueLabels) > 0 {
  251. if err = repo_module.InitializeLabels(ctx, repo.ID, opts.IssueLabels, false); err != nil {
  252. rollbackRepo = repo
  253. rollbackRepo.OwnerID = u.ID
  254. return fmt.Errorf("InitializeLabels: %w", err)
  255. }
  256. }
  257. if err := repo_module.CheckDaemonExportOK(ctx, repo); err != nil {
  258. return fmt.Errorf("checkDaemonExportOK: %w", err)
  259. }
  260. if stdout, _, err := git.NewCommand(ctx, "update-server-info").
  261. SetDescription(fmt.Sprintf("CreateRepository(git update-server-info): %s", repoPath)).
  262. RunStdString(&git.RunOpts{Dir: repoPath}); err != nil {
  263. log.Error("CreateRepository(git update-server-info) in %v: Stdout: %s\nError: %v", repo, stdout, err)
  264. rollbackRepo = repo
  265. rollbackRepo.OwnerID = u.ID
  266. return fmt.Errorf("CreateRepository(git update-server-info): %w", err)
  267. }
  268. return nil
  269. }); err != nil {
  270. if rollbackRepo != nil {
  271. if errDelete := DeleteRepositoryDirectly(ctx, doer, rollbackRepo.ID); errDelete != nil {
  272. log.Error("Rollback deleteRepository: %v", errDelete)
  273. }
  274. }
  275. return nil, err
  276. }
  277. return repo, nil
  278. }