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.

migrate.go 7.6KB

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