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.

migrate.go 8.0 kB

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>
5 years ago
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235
  1. // Copyright 2020 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 repo
  5. import (
  6. "bytes"
  7. "errors"
  8. "fmt"
  9. "net/http"
  10. "strings"
  11. "code.gitea.io/gitea/models"
  12. "code.gitea.io/gitea/modules/auth"
  13. "code.gitea.io/gitea/modules/context"
  14. "code.gitea.io/gitea/modules/convert"
  15. "code.gitea.io/gitea/modules/graceful"
  16. "code.gitea.io/gitea/modules/log"
  17. "code.gitea.io/gitea/modules/migrations"
  18. "code.gitea.io/gitea/modules/migrations/base"
  19. "code.gitea.io/gitea/modules/notification"
  20. repo_module "code.gitea.io/gitea/modules/repository"
  21. "code.gitea.io/gitea/modules/setting"
  22. api "code.gitea.io/gitea/modules/structs"
  23. "code.gitea.io/gitea/modules/util"
  24. )
  25. // Migrate migrate remote git repository to gitea
  26. func Migrate(ctx *context.APIContext, form api.MigrateRepoOptions) {
  27. // swagger:operation POST /repos/migrate repository repoMigrate
  28. // ---
  29. // summary: Migrate a remote git repository
  30. // consumes:
  31. // - application/json
  32. // produces:
  33. // - application/json
  34. // parameters:
  35. // - name: body
  36. // in: body
  37. // schema:
  38. // "$ref": "#/definitions/MigrateRepoOptions"
  39. // responses:
  40. // "201":
  41. // "$ref": "#/responses/Repository"
  42. // "403":
  43. // "$ref": "#/responses/forbidden"
  44. // "422":
  45. // "$ref": "#/responses/validationError"
  46. //get repoOwner
  47. var (
  48. repoOwner *models.User
  49. err error
  50. )
  51. if len(form.RepoOwner) != 0 {
  52. repoOwner, err = models.GetUserByName(form.RepoOwner)
  53. } else if form.RepoOwnerID != 0 {
  54. repoOwner, err = models.GetUserByID(form.RepoOwnerID)
  55. } else {
  56. repoOwner = ctx.User
  57. }
  58. if err != nil {
  59. if models.IsErrUserNotExist(err) {
  60. ctx.Error(http.StatusUnprocessableEntity, "", err)
  61. } else {
  62. ctx.Error(http.StatusInternalServerError, "GetUser", err)
  63. }
  64. return
  65. }
  66. if ctx.HasError() {
  67. ctx.Error(http.StatusUnprocessableEntity, "", ctx.GetErrMsg())
  68. return
  69. }
  70. if !ctx.User.IsAdmin {
  71. if !repoOwner.IsOrganization() && ctx.User.ID != repoOwner.ID {
  72. ctx.Error(http.StatusForbidden, "", "Given user is not an organization.")
  73. return
  74. }
  75. if repoOwner.IsOrganization() {
  76. // Check ownership of organization.
  77. isOwner, err := repoOwner.IsOwnedBy(ctx.User.ID)
  78. if err != nil {
  79. ctx.Error(http.StatusInternalServerError, "IsOwnedBy", err)
  80. return
  81. } else if !isOwner {
  82. ctx.Error(http.StatusForbidden, "", "Given user is not owner of organization.")
  83. return
  84. }
  85. }
  86. }
  87. remoteAddr, err := auth.ParseRemoteAddr(form.CloneAddr, form.AuthUsername, form.AuthPassword, ctx.User)
  88. if err != nil {
  89. if models.IsErrInvalidCloneAddr(err) {
  90. addrErr := err.(models.ErrInvalidCloneAddr)
  91. switch {
  92. case addrErr.IsURLError:
  93. ctx.Error(http.StatusUnprocessableEntity, "", err)
  94. case addrErr.IsPermissionDenied:
  95. ctx.Error(http.StatusUnprocessableEntity, "", "You are not allowed to import local repositories.")
  96. case addrErr.IsInvalidPath:
  97. ctx.Error(http.StatusUnprocessableEntity, "", "Invalid local path, it does not exist or not a directory.")
  98. default:
  99. ctx.Error(http.StatusInternalServerError, "ParseRemoteAddr", "Unknown error type (ErrInvalidCloneAddr): "+err.Error())
  100. }
  101. } else {
  102. ctx.Error(http.StatusInternalServerError, "ParseRemoteAddr", err)
  103. }
  104. return
  105. }
  106. gitServiceType := convert.ToGitServiceType(form.Service)
  107. if form.Mirror && setting.Repository.DisableMirrors {
  108. ctx.Error(http.StatusForbidden, "MirrorsGlobalDisabled", fmt.Errorf("the site administrator has disabled mirrors"))
  109. return
  110. }
  111. if setting.Repository.DisableMigrations {
  112. ctx.Error(http.StatusForbidden, "MigrationsGlobalDisabled", fmt.Errorf("the site administrator has disabled migrations"))
  113. return
  114. }
  115. var opts = migrations.MigrateOptions{
  116. CloneAddr: remoteAddr,
  117. RepoName: form.RepoName,
  118. Description: form.Description,
  119. Private: form.Private || setting.Repository.ForcePrivate,
  120. Mirror: form.Mirror,
  121. AuthUsername: form.AuthUsername,
  122. AuthPassword: form.AuthPassword,
  123. AuthToken: form.AuthToken,
  124. Wiki: form.Wiki,
  125. Issues: form.Issues,
  126. Milestones: form.Milestones,
  127. Labels: form.Labels,
  128. Comments: true,
  129. PullRequests: form.PullRequests,
  130. Releases: form.Releases,
  131. GitServiceType: gitServiceType,
  132. MirrorInterval: form.MirrorInterval,
  133. }
  134. if opts.Mirror {
  135. opts.Issues = false
  136. opts.Milestones = false
  137. opts.Labels = false
  138. opts.Comments = false
  139. opts.PullRequests = false
  140. opts.Releases = false
  141. }
  142. repo, err := repo_module.CreateRepository(ctx.User, repoOwner, models.CreateRepoOptions{
  143. Name: opts.RepoName,
  144. Description: opts.Description,
  145. OriginalURL: form.CloneAddr,
  146. GitServiceType: gitServiceType,
  147. IsPrivate: opts.Private,
  148. IsMirror: opts.Mirror,
  149. Status: models.RepositoryBeingMigrated,
  150. })
  151. if err != nil {
  152. handleMigrateError(ctx, repoOwner, remoteAddr, err)
  153. return
  154. }
  155. opts.MigrateToRepoID = repo.ID
  156. defer func() {
  157. if e := recover(); e != nil {
  158. var buf bytes.Buffer
  159. fmt.Fprintf(&buf, "Handler crashed with error: %v", log.Stack(2))
  160. err = errors.New(buf.String())
  161. }
  162. if err == nil {
  163. notification.NotifyMigrateRepository(ctx.User, repoOwner, repo)
  164. return
  165. }
  166. if repo != nil {
  167. if errDelete := models.DeleteRepository(ctx.User, repoOwner.ID, repo.ID); errDelete != nil {
  168. log.Error("DeleteRepository: %v", errDelete)
  169. }
  170. }
  171. }()
  172. if _, err = migrations.MigrateRepository(graceful.GetManager().HammerContext(), ctx.User, repoOwner.Name, opts); err != nil {
  173. handleMigrateError(ctx, repoOwner, remoteAddr, err)
  174. return
  175. }
  176. log.Trace("Repository migrated: %s/%s", repoOwner.Name, form.RepoName)
  177. ctx.JSON(http.StatusCreated, convert.ToRepo(repo, models.AccessModeAdmin))
  178. }
  179. func handleMigrateError(ctx *context.APIContext, repoOwner *models.User, remoteAddr string, err error) {
  180. switch {
  181. case models.IsErrRepoAlreadyExist(err):
  182. ctx.Error(http.StatusConflict, "", "The repository with the same name already exists.")
  183. case models.IsErrRepoFilesAlreadyExist(err):
  184. ctx.Error(http.StatusConflict, "", "Files already exist for this repository. Adopt them or delete them.")
  185. case migrations.IsRateLimitError(err):
  186. ctx.Error(http.StatusUnprocessableEntity, "", "Remote visit addressed rate limitation.")
  187. case migrations.IsTwoFactorAuthError(err):
  188. ctx.Error(http.StatusUnprocessableEntity, "", "Remote visit required two factors authentication.")
  189. case models.IsErrReachLimitOfRepo(err):
  190. ctx.Error(http.StatusUnprocessableEntity, "", fmt.Sprintf("You have already reached your limit of %d repositories.", repoOwner.MaxCreationLimit()))
  191. case models.IsErrNameReserved(err):
  192. ctx.Error(http.StatusUnprocessableEntity, "", fmt.Sprintf("The username '%s' is reserved.", err.(models.ErrNameReserved).Name))
  193. case models.IsErrNameCharsNotAllowed(err):
  194. ctx.Error(http.StatusUnprocessableEntity, "", fmt.Sprintf("The username '%s' contains invalid characters.", err.(models.ErrNameCharsNotAllowed).Name))
  195. case models.IsErrNamePatternNotAllowed(err):
  196. ctx.Error(http.StatusUnprocessableEntity, "", fmt.Sprintf("The pattern '%s' is not allowed in a username.", err.(models.ErrNamePatternNotAllowed).Pattern))
  197. case models.IsErrMigrationNotAllowed(err):
  198. ctx.Error(http.StatusUnprocessableEntity, "", err)
  199. case base.IsErrNotSupported(err):
  200. ctx.Error(http.StatusUnprocessableEntity, "", err)
  201. default:
  202. err = util.URLSanitizedError(err, remoteAddr)
  203. if strings.Contains(err.Error(), "Authentication failed") ||
  204. strings.Contains(err.Error(), "Bad credentials") ||
  205. strings.Contains(err.Error(), "could not read Username") {
  206. ctx.Error(http.StatusUnprocessableEntity, "", fmt.Sprintf("Authentication failed: %v.", err))
  207. } else if strings.Contains(err.Error(), "fatal:") {
  208. ctx.Error(http.StatusUnprocessableEntity, "", fmt.Sprintf("Migration failed: %v.", err))
  209. } else {
  210. ctx.Error(http.StatusInternalServerError, "MigrateRepository", err)
  211. }
  212. }
  213. }