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

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321
  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 = repo_module.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. if len(opts.DefaultBranch) > 0 {
  156. repo.DefaultBranch = opts.DefaultBranch
  157. gitRepo, err := gitrepo.OpenRepository(ctx, repo)
  158. if err != nil {
  159. return fmt.Errorf("openRepository: %w", err)
  160. }
  161. defer gitRepo.Close()
  162. if err = gitRepo.SetDefaultBranch(repo.DefaultBranch); err != nil {
  163. return fmt.Errorf("setDefaultBranch: %w", err)
  164. }
  165. if !repo.IsEmpty {
  166. if _, err := repo_module.SyncRepoBranches(ctx, repo.ID, u.ID); err != nil {
  167. return fmt.Errorf("SyncRepoBranches: %w", err)
  168. }
  169. }
  170. }
  171. if err = UpdateRepository(ctx, repo, false); err != nil {
  172. return fmt.Errorf("updateRepository: %w", err)
  173. }
  174. return nil
  175. }
  176. // CreateRepositoryDirectly creates a repository for the user/organization.
  177. func CreateRepositoryDirectly(ctx context.Context, doer, u *user_model.User, opts CreateRepoOptions) (*repo_model.Repository, error) {
  178. if !doer.IsAdmin && !u.CanCreateRepo() {
  179. return nil, repo_model.ErrReachLimitOfRepo{
  180. Limit: u.MaxRepoCreation,
  181. }
  182. }
  183. if len(opts.DefaultBranch) == 0 {
  184. opts.DefaultBranch = setting.Repository.DefaultBranch
  185. }
  186. // Check if label template exist
  187. if len(opts.IssueLabels) > 0 {
  188. if _, err := repo_module.LoadTemplateLabelsByDisplayName(opts.IssueLabels); err != nil {
  189. return nil, err
  190. }
  191. }
  192. if opts.ObjectFormatName == "" {
  193. opts.ObjectFormatName = git.Sha1ObjectFormat.Name()
  194. }
  195. repo := &repo_model.Repository{
  196. OwnerID: u.ID,
  197. Owner: u,
  198. OwnerName: u.Name,
  199. Name: opts.Name,
  200. LowerName: strings.ToLower(opts.Name),
  201. Description: opts.Description,
  202. OriginalURL: opts.OriginalURL,
  203. OriginalServiceType: opts.GitServiceType,
  204. IsPrivate: opts.IsPrivate,
  205. IsFsckEnabled: !opts.IsMirror,
  206. IsTemplate: opts.IsTemplate,
  207. CloseIssuesViaCommitInAnyBranch: setting.Repository.DefaultCloseIssuesViaCommitsInAnyBranch,
  208. Status: opts.Status,
  209. IsEmpty: !opts.AutoInit,
  210. TrustModel: opts.TrustModel,
  211. IsMirror: opts.IsMirror,
  212. DefaultBranch: opts.DefaultBranch,
  213. ObjectFormatName: opts.ObjectFormatName,
  214. }
  215. var rollbackRepo *repo_model.Repository
  216. if err := db.WithTx(ctx, func(ctx context.Context) error {
  217. if err := repo_module.CreateRepositoryByExample(ctx, doer, u, repo, false, false); err != nil {
  218. return err
  219. }
  220. // No need for init mirror.
  221. if opts.IsMirror {
  222. return nil
  223. }
  224. repoPath := repo_model.RepoPath(u.Name, repo.Name)
  225. isExist, err := util.IsExist(repoPath)
  226. if err != nil {
  227. log.Error("Unable to check if %s exists. Error: %v", repoPath, err)
  228. return err
  229. }
  230. if isExist {
  231. // repo already exists - We have two or three options.
  232. // 1. We fail stating that the directory exists
  233. // 2. We create the db repository to go with this data and adopt the git repo
  234. // 3. We delete it and start afresh
  235. //
  236. // Previously Gitea would just delete and start afresh - this was naughty.
  237. // So we will now fail and delegate to other functionality to adopt or delete
  238. log.Error("Files already exist in %s and we are not going to adopt or delete.", repoPath)
  239. return repo_model.ErrRepoFilesAlreadyExist{
  240. Uname: u.Name,
  241. Name: repo.Name,
  242. }
  243. }
  244. if err = initRepository(ctx, repoPath, doer, repo, opts); err != nil {
  245. if err2 := util.RemoveAll(repoPath); err2 != nil {
  246. log.Error("initRepository: %v", err)
  247. return fmt.Errorf(
  248. "delete repo directory %s/%s failed(2): %v", u.Name, repo.Name, err2)
  249. }
  250. return fmt.Errorf("initRepository: %w", err)
  251. }
  252. // Initialize Issue Labels if selected
  253. if len(opts.IssueLabels) > 0 {
  254. if err = repo_module.InitializeLabels(ctx, repo.ID, opts.IssueLabels, false); err != nil {
  255. rollbackRepo = repo
  256. rollbackRepo.OwnerID = u.ID
  257. return fmt.Errorf("InitializeLabels: %w", err)
  258. }
  259. }
  260. if err := repo_module.CheckDaemonExportOK(ctx, repo); err != nil {
  261. return fmt.Errorf("checkDaemonExportOK: %w", err)
  262. }
  263. if stdout, _, err := git.NewCommand(ctx, "update-server-info").
  264. SetDescription(fmt.Sprintf("CreateRepository(git update-server-info): %s", repoPath)).
  265. RunStdString(&git.RunOpts{Dir: repoPath}); err != nil {
  266. log.Error("CreateRepository(git update-server-info) in %v: Stdout: %s\nError: %v", repo, stdout, err)
  267. rollbackRepo = repo
  268. rollbackRepo.OwnerID = u.ID
  269. return fmt.Errorf("CreateRepository(git update-server-info): %w", err)
  270. }
  271. return nil
  272. }); err != nil {
  273. if rollbackRepo != nil {
  274. if errDelete := DeleteRepositoryDirectly(ctx, doer, rollbackRepo.ID); errDelete != nil {
  275. log.Error("Rollback deleteRepository: %v", errDelete)
  276. }
  277. }
  278. return nil, err
  279. }
  280. return repo, nil
  281. }