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.

Adopt repositories (#12920) * Don't automatically delete repository files if they are present Prior to this PR Gitea would delete any repository files if they are present during creation or migration. This can in certain circumstances lead to data-loss and is slightly unpleasant. This PR provides a mechanism for Gitea to adopt repositories on creation and otherwise requires an explicit flag for deletion. PushCreate is slightly different - the create will cause adoption if that is allowed otherwise it will delete the data if that is allowed. Signed-off-by: Andrew Thornton <art27@cantab.net> * Update swagger Signed-off-by: Andrew Thornton <art27@cantab.net> * Fix tests and migrate overwrite Signed-off-by: Andrew Thornton <art27@cantab.net> * as per @lunny Only offer to adopt or overwrite if the user can do that. Allow the site administrator to adopt or overwrite in all circumstances Signed-off-by: Andrew Thornton <art27@cantab.net> * Use setting.Repository.DefaultBranch for the default branch Signed-off-by: Andrew Thornton <art27@cantab.net> * Always set setting.Repository.DefaultBranch Signed-off-by: Andrew Thornton <art27@cantab.net> * update swagger Signed-off-by: Andrew Thornton <art27@cantab.net> * update templates Signed-off-by: Andrew Thornton <art27@cantab.net> * ensure repo closed Signed-off-by: Andrew Thornton <art27@cantab.net> * Rewrite of adoption as per @6543 and @lunny Signed-off-by: Andrew Thornton <art27@cantab.net> * Apply suggestions from code review * update swagger Signed-off-by: Andrew Thornton <art27@cantab.net> * missing not Signed-off-by: Andrew Thornton <art27@cantab.net> * add modals and flash reporting Signed-off-by: Andrew Thornton <art27@cantab.net> * Make the unadopted page searchable Signed-off-by: Andrew Thornton <art27@cantab.net> * Add API Signed-off-by: Andrew Thornton <art27@cantab.net> * Fix swagger Signed-off-by: Andrew Thornton <art27@cantab.net> * fix swagger Signed-off-by: Andrew Thornton <art27@cantab.net> * Handle empty and non-master branched repositories Signed-off-by: Andrew Thornton <art27@cantab.net> * placate lint Signed-off-by: Andrew Thornton <art27@cantab.net> * remove commented out code Signed-off-by: Andrew Thornton <art27@cantab.net> Co-authored-by: techknowlogick <techknowlogick@gitea.io>
3 years ago
Adopt repositories (#12920) * Don't automatically delete repository files if they are present Prior to this PR Gitea would delete any repository files if they are present during creation or migration. This can in certain circumstances lead to data-loss and is slightly unpleasant. This PR provides a mechanism for Gitea to adopt repositories on creation and otherwise requires an explicit flag for deletion. PushCreate is slightly different - the create will cause adoption if that is allowed otherwise it will delete the data if that is allowed. Signed-off-by: Andrew Thornton <art27@cantab.net> * Update swagger Signed-off-by: Andrew Thornton <art27@cantab.net> * Fix tests and migrate overwrite Signed-off-by: Andrew Thornton <art27@cantab.net> * as per @lunny Only offer to adopt or overwrite if the user can do that. Allow the site administrator to adopt or overwrite in all circumstances Signed-off-by: Andrew Thornton <art27@cantab.net> * Use setting.Repository.DefaultBranch for the default branch Signed-off-by: Andrew Thornton <art27@cantab.net> * Always set setting.Repository.DefaultBranch Signed-off-by: Andrew Thornton <art27@cantab.net> * update swagger Signed-off-by: Andrew Thornton <art27@cantab.net> * update templates Signed-off-by: Andrew Thornton <art27@cantab.net> * ensure repo closed Signed-off-by: Andrew Thornton <art27@cantab.net> * Rewrite of adoption as per @6543 and @lunny Signed-off-by: Andrew Thornton <art27@cantab.net> * Apply suggestions from code review * update swagger Signed-off-by: Andrew Thornton <art27@cantab.net> * missing not Signed-off-by: Andrew Thornton <art27@cantab.net> * add modals and flash reporting Signed-off-by: Andrew Thornton <art27@cantab.net> * Make the unadopted page searchable Signed-off-by: Andrew Thornton <art27@cantab.net> * Add API Signed-off-by: Andrew Thornton <art27@cantab.net> * Fix swagger Signed-off-by: Andrew Thornton <art27@cantab.net> * fix swagger Signed-off-by: Andrew Thornton <art27@cantab.net> * Handle empty and non-master branched repositories Signed-off-by: Andrew Thornton <art27@cantab.net> * placate lint Signed-off-by: Andrew Thornton <art27@cantab.net> * remove commented out code Signed-off-by: Andrew Thornton <art27@cantab.net> Co-authored-by: techknowlogick <techknowlogick@gitea.io>
3 years ago
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228
  1. // Copyright 2019 The Gitea Authors. All rights reserved.
  2. // SPDX-License-Identifier: MIT
  3. package repository
  4. import (
  5. "context"
  6. "fmt"
  7. "strings"
  8. "time"
  9. "code.gitea.io/gitea/models/db"
  10. git_model "code.gitea.io/gitea/models/git"
  11. repo_model "code.gitea.io/gitea/models/repo"
  12. user_model "code.gitea.io/gitea/models/user"
  13. "code.gitea.io/gitea/modules/git"
  14. "code.gitea.io/gitea/modules/log"
  15. repo_module "code.gitea.io/gitea/modules/repository"
  16. "code.gitea.io/gitea/modules/structs"
  17. "code.gitea.io/gitea/modules/util"
  18. notify_service "code.gitea.io/gitea/services/notify"
  19. )
  20. // ErrForkAlreadyExist represents a "ForkAlreadyExist" kind of error.
  21. type ErrForkAlreadyExist struct {
  22. Uname string
  23. RepoName string
  24. ForkName string
  25. }
  26. // IsErrForkAlreadyExist checks if an error is an ErrForkAlreadyExist.
  27. func IsErrForkAlreadyExist(err error) bool {
  28. _, ok := err.(ErrForkAlreadyExist)
  29. return ok
  30. }
  31. func (err ErrForkAlreadyExist) Error() string {
  32. return fmt.Sprintf("repository is already forked by user [uname: %s, repo path: %s, fork path: %s]", err.Uname, err.RepoName, err.ForkName)
  33. }
  34. func (err ErrForkAlreadyExist) Unwrap() error {
  35. return util.ErrAlreadyExist
  36. }
  37. // ForkRepoOptions contains the fork repository options
  38. type ForkRepoOptions struct {
  39. BaseRepo *repo_model.Repository
  40. Name string
  41. Description string
  42. }
  43. // ForkRepository forks a repository
  44. func ForkRepository(ctx context.Context, doer, owner *user_model.User, opts ForkRepoOptions) (*repo_model.Repository, error) {
  45. // Fork is prohibited, if user has reached maximum limit of repositories
  46. if !owner.CanForkRepo() {
  47. return nil, repo_model.ErrReachLimitOfRepo{
  48. Limit: owner.MaxRepoCreation,
  49. }
  50. }
  51. forkedRepo, err := repo_model.GetUserFork(ctx, opts.BaseRepo.ID, owner.ID)
  52. if err != nil {
  53. return nil, err
  54. }
  55. if forkedRepo != nil {
  56. return nil, ErrForkAlreadyExist{
  57. Uname: owner.Name,
  58. RepoName: opts.BaseRepo.FullName(),
  59. ForkName: forkedRepo.FullName(),
  60. }
  61. }
  62. repo := &repo_model.Repository{
  63. OwnerID: owner.ID,
  64. Owner: owner,
  65. OwnerName: owner.Name,
  66. Name: opts.Name,
  67. LowerName: strings.ToLower(opts.Name),
  68. Description: opts.Description,
  69. DefaultBranch: opts.BaseRepo.DefaultBranch,
  70. IsPrivate: opts.BaseRepo.IsPrivate || opts.BaseRepo.Owner.Visibility == structs.VisibleTypePrivate,
  71. IsEmpty: opts.BaseRepo.IsEmpty,
  72. IsFork: true,
  73. ForkID: opts.BaseRepo.ID,
  74. }
  75. oldRepoPath := opts.BaseRepo.RepoPath()
  76. needsRollback := false
  77. rollbackFn := func() {
  78. if !needsRollback {
  79. return
  80. }
  81. repoPath := repo_model.RepoPath(owner.Name, repo.Name)
  82. if exists, _ := util.IsExist(repoPath); !exists {
  83. return
  84. }
  85. // As the transaction will be failed and hence database changes will be destroyed we only need
  86. // to delete the related repository on the filesystem
  87. if errDelete := util.RemoveAll(repoPath); errDelete != nil {
  88. log.Error("Failed to remove fork repo")
  89. }
  90. }
  91. needsRollbackInPanic := true
  92. defer func() {
  93. panicErr := recover()
  94. if panicErr == nil {
  95. return
  96. }
  97. if needsRollbackInPanic {
  98. rollbackFn()
  99. }
  100. panic(panicErr)
  101. }()
  102. err = db.WithTx(ctx, func(txCtx context.Context) error {
  103. if err = repo_module.CreateRepositoryByExample(txCtx, doer, owner, repo, false, true); err != nil {
  104. return err
  105. }
  106. if err = repo_model.IncrementRepoForkNum(txCtx, opts.BaseRepo.ID); err != nil {
  107. return err
  108. }
  109. // copy lfs files failure should not be ignored
  110. if err = git_model.CopyLFS(txCtx, repo, opts.BaseRepo); err != nil {
  111. return err
  112. }
  113. needsRollback = true
  114. repoPath := repo_model.RepoPath(owner.Name, repo.Name)
  115. if stdout, _, err := git.NewCommand(txCtx,
  116. "clone", "--bare").AddDynamicArguments(oldRepoPath, repoPath).
  117. SetDescription(fmt.Sprintf("ForkRepository(git clone): %s to %s", opts.BaseRepo.FullName(), repo.FullName())).
  118. RunStdBytes(&git.RunOpts{Timeout: 10 * time.Minute}); err != nil {
  119. log.Error("Fork Repository (git clone) Failed for %v (from %v):\nStdout: %s\nError: %v", repo, opts.BaseRepo, stdout, err)
  120. return fmt.Errorf("git clone: %w", err)
  121. }
  122. if err := repo_module.CheckDaemonExportOK(txCtx, repo); err != nil {
  123. return fmt.Errorf("checkDaemonExportOK: %w", err)
  124. }
  125. if stdout, _, err := git.NewCommand(txCtx, "update-server-info").
  126. SetDescription(fmt.Sprintf("ForkRepository(git update-server-info): %s", repo.FullName())).
  127. RunStdString(&git.RunOpts{Dir: repoPath}); err != nil {
  128. log.Error("Fork Repository (git update-server-info) failed for %v:\nStdout: %s\nError: %v", repo, stdout, err)
  129. return fmt.Errorf("git update-server-info: %w", err)
  130. }
  131. if err = repo_module.CreateDelegateHooks(repoPath); err != nil {
  132. return fmt.Errorf("createDelegateHooks: %w", err)
  133. }
  134. gitRepo, err := git.OpenRepository(txCtx, repo.RepoPath())
  135. if err != nil {
  136. return fmt.Errorf("OpenRepository: %w", err)
  137. }
  138. defer gitRepo.Close()
  139. _, err = repo_module.SyncRepoBranchesWithRepo(txCtx, repo, gitRepo, doer.ID)
  140. return err
  141. })
  142. needsRollbackInPanic = false
  143. if err != nil {
  144. rollbackFn()
  145. return nil, err
  146. }
  147. // even if below operations failed, it could be ignored. And they will be retried
  148. if err := repo_module.UpdateRepoSize(ctx, repo); err != nil {
  149. log.Error("Failed to update size for repository: %v", err)
  150. }
  151. if err := repo_model.CopyLanguageStat(opts.BaseRepo, repo); err != nil {
  152. log.Error("Copy language stat from oldRepo failed: %v", err)
  153. }
  154. gitRepo, err := git.OpenRepository(ctx, repo.RepoPath())
  155. if err != nil {
  156. log.Error("Open created git repository failed: %v", err)
  157. } else {
  158. defer gitRepo.Close()
  159. if err := repo_module.SyncReleasesWithTags(ctx, repo, gitRepo); err != nil {
  160. log.Error("Sync releases from git tags failed: %v", err)
  161. }
  162. }
  163. notify_service.ForkRepository(ctx, doer, opts.BaseRepo, repo)
  164. return repo, nil
  165. }
  166. // ConvertForkToNormalRepository convert the provided repo from a forked repo to normal repo
  167. func ConvertForkToNormalRepository(ctx context.Context, repo *repo_model.Repository) error {
  168. err := db.WithTx(ctx, func(ctx context.Context) error {
  169. repo, err := repo_model.GetRepositoryByID(ctx, repo.ID)
  170. if err != nil {
  171. return err
  172. }
  173. if !repo.IsFork {
  174. return nil
  175. }
  176. if err := repo_model.DecrementRepoForkNum(ctx, repo.ForkID); err != nil {
  177. log.Error("Unable to decrement repo fork num for old root repo %d of repository %-v whilst converting from fork. Error: %v", repo.ForkID, repo, err)
  178. return err
  179. }
  180. repo.IsFork = false
  181. repo.ForkID = 0
  182. if err := repo_module.UpdateRepository(ctx, repo, false); err != nil {
  183. log.Error("Unable to update repository %-v whilst converting from fork. Error: %v", repo, err)
  184. return err
  185. }
  186. return nil
  187. })
  188. return err
  189. }