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