From d1af47355378adc1af164770083873095cca5c6a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=98=99=E2=97=A6=20The=20Tablet=20=E2=9D=80=20GamerGirla?= =?UTF-8?q?ndCo=20=E2=97=A6=E2=9D=A7?= Date: Sun, 17 Aug 2025 19:36:48 -0400 Subject: [PATCH] update repository storage layout as per https://github.com/go-gitea/gitea/issues/1872#issuecomment-3194681583 --- models/repo/repo.go | 65 ++++++++++---- models/repo/transfer.go | 2 +- models/repo/wiki.go | 16 ++-- routers/api/v1/admin/adopt.go | 144 +++++++++++++++++++++--------- routers/api/v1/api.go | 25 +++++- routers/web/admin/repos.go | 11 ++- routers/web/githttp.go | 2 +- routers/web/goget.go | 19 ++-- routers/web/user/setting/adopt.go | 15 +++- routers/web/web.go | 1 + services/context/repo.go | 25 +++++- services/repository/adopt.go | 6 +- services/repository/create.go | 2 +- services/repository/transfer.go | 24 ++--- 14 files changed, 252 insertions(+), 105 deletions(-) diff --git a/models/repo/repo.go b/models/repo/repo.go index a7e3aa0060..18cc6bc6ba 100644 --- a/models/repo/repo.go +++ b/models/repo/repo.go @@ -228,13 +228,21 @@ func init() { db.RegisterModel(new(Repository)) } -func RelativePath(ownerName, repoName string) string { - return strings.ToLower(ownerName) + "/" + strings.ToLower(repoName) + ".git" +func RelativePathBaseName(ownerName, repoName string, groupID int64) string { + var groupSegment string + if groupID > 0 { + groupSegment = strconv.FormatInt(groupID, 10) + "/" + } + return strings.ToLower(ownerName) + "/" + groupSegment + strings.ToLower(repoName) +} + +func RelativePath(ownerName, repoName string, groupID int64) string { + return RelativePathBaseName(ownerName, repoName, groupID) + ".git" } // RelativePath should be an unix style path like username/reponame.git func (repo *Repository) RelativePath() string { - return RelativePath(repo.OwnerName, repo.Name) + return RelativePath(repo.OwnerName, repo.Name, repo.GroupID) } type StorageRepo string @@ -582,13 +590,19 @@ func (repo *Repository) IsGenerated() bool { } // RepoPath returns repository path by given user and repository name. -func RepoPath(userName, repoName string) string { //revive:disable-line:exported - return filepath.Join(setting.RepoRootPath, filepath.Clean(strings.ToLower(userName)), filepath.Clean(strings.ToLower(repoName)+".git")) +func RepoPath(userName, repoName string, groupID int64) string { //revive:disable-line:exported + var joinArgs []string + joinArgs = append(joinArgs, user_model.UserPath(userName)) + if groupID > 0 { + joinArgs = append(joinArgs, strconv.FormatInt(groupID, 10)) + } + joinArgs = append(joinArgs, strings.ToLower(repoName)+".git") + return filepath.Join(joinArgs...) } // RepoPath returns the repository path func (repo *Repository) RepoPath() string { - return RepoPath(repo.OwnerName, repo.Name) + return RepoPath(repo.OwnerName, repo.Name, repo.GroupID) } // Link returns the repository relative url @@ -658,13 +672,25 @@ type CloneLink struct { Tea string } +func getGroupSegment(gid int64) string { + var groupSegment string + if gid > 0 { + groupSegment = fmt.Sprintf("%d", gid) + } + return groupSegment +} + +func groupSegmentWithTrailingSlash(gid int64) string { + return getGroupSegment(gid) + "/" +} + // ComposeHTTPSCloneURL returns HTTPS clone URL based on the given owner and repository name. -func ComposeHTTPSCloneURL(ctx context.Context, owner, repo string) string { - return fmt.Sprintf("%s%s/%s.git", httplib.GuessCurrentAppURL(ctx), url.PathEscape(owner), url.PathEscape(repo)) +func ComposeHTTPSCloneURL(ctx context.Context, owner, repo string, groupID int64) string { + return fmt.Sprintf("%s%s/%s%s.git", httplib.GuessCurrentAppURL(ctx), url.PathEscape(owner), groupSegmentWithTrailingSlash(groupID), url.PathEscape(repo)) } // ComposeSSHCloneURL returns SSH clone URL based on the given owner and repository name. -func ComposeSSHCloneURL(doer *user_model.User, ownerName, repoName string) string { +func ComposeSSHCloneURL(doer *user_model.User, ownerName, repoName string, groupID int64) string { sshUser := setting.SSH.User sshDomain := setting.SSH.Domain @@ -683,7 +709,7 @@ func ComposeSSHCloneURL(doer *user_model.User, ownerName, repoName string) strin // non-standard port, it must use full URI if setting.SSH.Port != 22 { sshHost := net.JoinHostPort(sshDomain, strconv.Itoa(setting.SSH.Port)) - return fmt.Sprintf("ssh://%s@%s/%s/%s.git", sshUser, sshHost, url.PathEscape(ownerName), url.PathEscape(repoName)) + return fmt.Sprintf("ssh://%s@%s/%s%s/%s.git", sshUser, sshHost, url.PathEscape(ownerName), groupSegmentWithTrailingSlash(groupID), url.PathEscape(repoName)) } // for standard port, it can use a shorter URI (without the port) @@ -698,25 +724,25 @@ func ComposeSSHCloneURL(doer *user_model.User, ownerName, repoName string) strin } // ComposeTeaCloneCommand returns Tea CLI clone command based on the given owner and repository name. -func ComposeTeaCloneCommand(ctx context.Context, owner, repo string) string { - return fmt.Sprintf("tea clone %s/%s", url.PathEscape(owner), url.PathEscape(repo)) +func ComposeTeaCloneCommand(ctx context.Context, owner, repo string, groupID int64) string { + return fmt.Sprintf("tea clone %s/%s%s", url.PathEscape(owner), url.PathEscape(repo), groupSegmentWithTrailingSlash(groupID)) } -func (repo *Repository) cloneLink(ctx context.Context, doer *user_model.User, repoPathName string) *CloneLink { +func (repo *Repository) cloneLink(ctx context.Context, doer *user_model.User, repoPathName string, groupID int64) *CloneLink { return &CloneLink{ - SSH: ComposeSSHCloneURL(doer, repo.OwnerName, repoPathName), - HTTPS: ComposeHTTPSCloneURL(ctx, repo.OwnerName, repoPathName), - Tea: ComposeTeaCloneCommand(ctx, repo.OwnerName, repoPathName), + SSH: ComposeSSHCloneURL(doer, repo.OwnerName, repoPathName, groupID), + HTTPS: ComposeHTTPSCloneURL(ctx, repo.OwnerName, repoPathName, groupID), + Tea: ComposeTeaCloneCommand(ctx, repo.OwnerName, repoPathName, groupID), } } // CloneLink returns clone URLs of repository. func (repo *Repository) CloneLink(ctx context.Context, doer *user_model.User) (cl *CloneLink) { - return repo.cloneLink(ctx, doer, repo.Name) + return repo.cloneLink(ctx, doer, repo.Name, repo.GroupID) } func (repo *Repository) CloneLinkGeneral(ctx context.Context) (cl *CloneLink) { - return repo.cloneLink(ctx, nil /* no doer, use a general git user */, repo.Name) + return repo.cloneLink(ctx, nil /* no doer, use a general git user */, repo.Name, repo.GroupID) } // GetOriginalURLHostname returns the hostname of a URL or the URL @@ -854,10 +880,11 @@ func GetRepositoriesMapByIDs(ctx context.Context, ids []int64) (map[int64]*Repos return repos, db.GetEngine(ctx).In("id", ids).Find(&repos) } -func IsRepositoryModelExist(ctx context.Context, u *user_model.User, repoName string) (bool, error) { +func IsRepositoryModelExist(ctx context.Context, u *user_model.User, repoName string, groupID int64) (bool, error) { return db.GetEngine(ctx).Get(&Repository{ OwnerID: u.ID, LowerName: strings.ToLower(repoName), + GroupID: groupID, }) } diff --git a/models/repo/transfer.go b/models/repo/transfer.go index 3fb8cb69ab..f611da1d23 100644 --- a/models/repo/transfer.go +++ b/models/repo/transfer.go @@ -254,7 +254,7 @@ func CreatePendingRepositoryTransfer(ctx context.Context, doer, newOwner *user_m } // Check if new owner has repository with same name. - if has, err := IsRepositoryModelExist(ctx, newOwner, repo.Name); err != nil { + if has, err := IsRepositoryModelExist(ctx, newOwner, repo.Name, repo.GroupID); err != nil { return fmt.Errorf("IsRepositoryExist: %w", err) } else if has { return ErrRepoAlreadyExist{ diff --git a/models/repo/wiki.go b/models/repo/wiki.go index 47c8fa43ab..487cc953be 100644 --- a/models/repo/wiki.go +++ b/models/repo/wiki.go @@ -5,12 +5,10 @@ package repo import ( - "context" - "fmt" - "strings" - user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/modules/util" + "context" + "fmt" ) // ErrWikiAlreadyExist represents a "WikiAlreadyExist" kind of error. @@ -72,15 +70,13 @@ func (err ErrWikiInvalidFileName) Unwrap() error { // WikiCloneLink returns clone URLs of repository wiki. func (repo *Repository) WikiCloneLink(ctx context.Context, doer *user_model.User) *CloneLink { - return repo.cloneLink(ctx, doer, repo.Name+".wiki") + return repo.cloneLink(ctx, doer, repo.Name+".wiki", repo.GroupID) } -func RelativeWikiPath(ownerName, repoName string) string { - return strings.ToLower(ownerName) + "/" + strings.ToLower(repoName) + ".wiki.git" +func RelativeWikiPath(ownerName, repoName string, groupID int64) string { + return RelativePathBaseName(ownerName, repoName, groupID) + ".wiki.git" } -// WikiStorageRepo returns the storage repo for the wiki -// The wiki repository should have the same object format as the code repository func (repo *Repository) WikiStorageRepo() StorageRepo { - return StorageRepo(RelativeWikiPath(repo.OwnerName, repo.Name)) + return StorageRepo(RelativeWikiPath(repo.OwnerName, repo.Name, repo.GroupID)) } diff --git a/routers/api/v1/admin/adopt.go b/routers/api/v1/admin/adopt.go index 9f1175a1ff..0399a1107f 100644 --- a/routers/api/v1/admin/adopt.go +++ b/routers/api/v1/admin/adopt.go @@ -55,6 +55,47 @@ func ListUnadoptedRepositories(ctx *context.APIContext) { ctx.JSON(http.StatusOK, repoNames) } +func commonAdoptRepository(ctx *context.APIContext) { + ownerName := ctx.PathParam("username") + repoName := ctx.PathParam("reponame") + groupID := ctx.PathParamInt64("group_id") + + ctxUser, err := user_model.GetUserByName(ctx, ownerName) + if err != nil { + if user_model.IsErrUserNotExist(err) { + ctx.APIErrorNotFound() + return + } + ctx.APIErrorInternal(err) + return + } + + // check not a repo + has, err := repo_model.IsRepositoryModelExist(ctx, ctxUser, repoName, groupID) + if err != nil { + ctx.APIErrorInternal(err) + return + } + exist, err := gitrepo.IsRepositoryExist(ctx, repo_model.StorageRepo(repo_model.RelativePath(ctxUser.Name, repoName, groupID))) + if err != nil { + ctx.APIErrorInternal(err) + return + } + if has || !exist { + ctx.APIErrorNotFound() + return + } + if _, err := repo_service.AdoptRepository(ctx, ctx.Doer, ctxUser, repo_service.CreateRepoOptions{ + Name: repoName, + IsPrivate: true, + }); err != nil { + ctx.APIErrorInternal(err) + return + } + + ctx.Status(http.StatusNoContent) +} + // AdoptRepository will adopt an unadopted repository func AdoptRepository(ctx *context.APIContext) { // swagger:operation POST /admin/unadopted/{owner}/{repo} admin adminAdoptRepository @@ -80,8 +121,40 @@ func AdoptRepository(ctx *context.APIContext) { // "$ref": "#/responses/notFound" // "403": // "$ref": "#/responses/forbidden" + commonAdoptRepository(ctx) +} + +func AdoptGroupRepository(ctx *context.APIContext) { + // swagger:operation POST /admin/unadopted/{owner}/{group_id}/{repo} admin adminAdoptRepository + // --- + // summary: Adopt unadopted files as a repository + // produces: + // - application/json + // parameters: + // - name: owner + // in: path + // description: owner of the repo + // type: string + // required: true + // - name: repo + // in: path + // description: name of the repo + // type: string + // required: true + // responses: + // "204": + // "$ref": "#/responses/empty" + // "404": + // "$ref": "#/responses/notFound" + // "403": + // "$ref": "#/responses/forbidden" + commonAdoptRepository(ctx) +} + +func commonDeleteUnadoptedRepo(ctx *context.APIContext) { ownerName := ctx.PathParam("username") repoName := ctx.PathParam("reponame") + groupID := ctx.PathParamInt64("group_id") ctxUser, err := user_model.GetUserByName(ctx, ownerName) if err != nil { @@ -94,12 +167,12 @@ func AdoptRepository(ctx *context.APIContext) { } // check not a repo - has, err := repo_model.IsRepositoryModelExist(ctx, ctxUser, repoName) + has, err := repo_model.IsRepositoryModelExist(ctx, ctxUser, repoName, groupID) if err != nil { ctx.APIErrorInternal(err) return } - exist, err := gitrepo.IsRepositoryExist(ctx, repo_model.StorageRepo(repo_model.RelativePath(ctxUser.Name, repoName))) + exist, err := gitrepo.IsRepositoryExist(ctx, repo_model.StorageRepo(repo_model.RelativePath(ctxUser.Name, repoName, groupID))) if err != nil { ctx.APIErrorInternal(err) return @@ -108,10 +181,8 @@ func AdoptRepository(ctx *context.APIContext) { ctx.APIErrorNotFound() return } - if _, err := repo_service.AdoptRepository(ctx, ctx.Doer, ctxUser, repo_service.CreateRepoOptions{ - Name: repoName, - IsPrivate: true, - }); err != nil { + + if err := repo_service.DeleteUnadoptedRepository(ctx, ctx.Doer, ctxUser, repoName, groupID); err != nil { ctx.APIErrorInternal(err) return } @@ -142,39 +213,30 @@ func DeleteUnadoptedRepository(ctx *context.APIContext) { // "$ref": "#/responses/empty" // "403": // "$ref": "#/responses/forbidden" - ownerName := ctx.PathParam("username") - repoName := ctx.PathParam("reponame") - - ctxUser, err := user_model.GetUserByName(ctx, ownerName) - if err != nil { - if user_model.IsErrUserNotExist(err) { - ctx.APIErrorNotFound() - return - } - ctx.APIErrorInternal(err) - return - } - - // check not a repo - has, err := repo_model.IsRepositoryModelExist(ctx, ctxUser, repoName) - if err != nil { - ctx.APIErrorInternal(err) - return - } - exist, err := gitrepo.IsRepositoryExist(ctx, repo_model.StorageRepo(repo_model.RelativePath(ctxUser.Name, repoName))) - if err != nil { - ctx.APIErrorInternal(err) - return - } - if has || !exist { - ctx.APIErrorNotFound() - return - } - - if err := repo_service.DeleteUnadoptedRepository(ctx, ctx.Doer, ctxUser, repoName); err != nil { - ctx.APIErrorInternal(err) - return - } - - ctx.Status(http.StatusNoContent) + commonDeleteUnadoptedRepo(ctx) +} + +func DeleteUnadoptedRepositoryInGroup(ctx *context.APIContext) { + // swagger:operation DELETE /admin/unadopted/{owner}/{group_id}/{repo} admin adminDeleteUnadoptedRepository + // --- + // summary: Delete unadopted files + // produces: + // - application/json + // parameters: + // - name: owner + // in: path + // description: owner of the repo + // type: string + // required: true + // - name: repo + // in: path + // description: name of the repo + // type: string + // required: true + // responses: + // "204": + // "$ref": "#/responses/empty" + // "403": + // "$ref": "#/responses/forbidden" + commonDeleteUnadoptedRepo(ctx) } diff --git a/routers/api/v1/api.go b/routers/api/v1/api.go index b17505bbff..75f9f5d52e 100644 --- a/routers/api/v1/api.go +++ b/routers/api/v1/api.go @@ -66,6 +66,7 @@ import ( "errors" "fmt" "net/http" + "strconv" "strings" actions_model "code.gitea.io/gitea/models/actions" @@ -140,7 +141,16 @@ func repoAssignment() func(ctx *context.APIContext) { return func(ctx *context.APIContext) { userName := ctx.PathParam("username") repoName := ctx.PathParam("reponame") - + var gid int64 + group := ctx.PathParam("group_id") + if group != "" { + gid, _ = strconv.ParseInt(group, 10, 64) + if gid == 0 { + ctx.Redirect(strings.Replace(ctx.Req.URL.RequestURI(), "/0/", "/", 1)) + return + } + group += "/" + } var ( owner *user_model.User err error @@ -186,6 +196,10 @@ func repoAssignment() func(ctx *context.APIContext) { } return } + if repo.GroupID != gid { + ctx.APIErrorNotFound() + return + } repo.Owner = owner ctx.Repo.Repository = repo @@ -1771,8 +1785,13 @@ func Routes() *web.Router { }) m.Group("/unadopted", func() { m.Get("", admin.ListUnadoptedRepositories) - m.Post("/{username}/{reponame}", admin.AdoptRepository) - m.Delete("/{username}/{reponame}", admin.DeleteUnadoptedRepository) + m.Group("/{username}", func() { + m.Post("/{reponame}", admin.AdoptRepository) + m.Delete("/{reponame}", admin.DeleteUnadoptedRepository) + m.Post("/{group_id}/{reponame}", admin.AdoptGroupRepository) + m.Delete("/{group_id}/{reponame}", admin.DeleteUnadoptedRepositoryInGroup) + }) + }) m.Group("/hooks", func() { m.Combo("").Get(admin.ListHooks). diff --git a/routers/web/admin/repos.go b/routers/web/admin/repos.go index 424219815c..799ee828d3 100644 --- a/routers/web/admin/repos.go +++ b/routers/web/admin/repos.go @@ -6,6 +6,7 @@ package admin import ( "net/http" "net/url" + "strconv" "strings" "code.gitea.io/gitea/models/db" @@ -127,14 +128,18 @@ func AdoptOrDeleteRepository(ctx *context.Context) { } repoName := dirSplit[1] + var groupID int64 + if len(dirSplit) >= 3 { + groupID, _ = strconv.ParseInt(dirSplit[2], 10, 64) + } // check not a repo - has, err := repo_model.IsRepositoryModelExist(ctx, ctxUser, repoName) + has, err := repo_model.IsRepositoryModelExist(ctx, ctxUser, repoName, groupID) if err != nil { ctx.ServerError("IsRepositoryExist", err) return } - exist, err := gitrepo.IsRepositoryExist(ctx, repo_model.StorageRepo(repo_model.RelativePath(ctxUser.Name, repoName))) + exist, err := gitrepo.IsRepositoryExist(ctx, repo_model.StorageRepo(repo_model.RelativePath(ctxUser.Name, repoName, groupID))) if err != nil { ctx.ServerError("IsDir", err) return @@ -151,7 +156,7 @@ func AdoptOrDeleteRepository(ctx *context.Context) { } ctx.Flash.Success(ctx.Tr("repo.adopt_preexisting_success", dir)) } else if action == "delete" { - if err := repo_service.DeleteUnadoptedRepository(ctx, ctx.Doer, ctxUser, dirSplit[1]); err != nil { + if err := repo_service.DeleteUnadoptedRepository(ctx, ctx.Doer, ctxUser, dirSplit[1], groupID); err != nil { ctx.ServerError("repository.AdoptRepository", err) return } diff --git a/routers/web/githttp.go b/routers/web/githttp.go index c738b68c8d..24af5e534d 100644 --- a/routers/web/githttp.go +++ b/routers/web/githttp.go @@ -9,7 +9,7 @@ import ( ) func addOwnerRepoGitHTTPRouters(m *web.Router, middlewares ...any) { - m.Group("/{username}/{reponame}", func() { + m.Group("/{username}/{group_id}?/{reponame}", func() { m.Methods("POST,OPTIONS", "/git-upload-pack", repo.ServiceUploadPack) m.Methods("POST,OPTIONS", "/git-receive-pack", repo.ServiceReceivePack) m.Methods("POST,OPTIONS", "/git-upload-archive", repo.ServiceUploadArchive) diff --git a/routers/web/goget.go b/routers/web/goget.go index 67e0bee866..6a769f973c 100644 --- a/routers/web/goget.go +++ b/routers/web/goget.go @@ -9,6 +9,7 @@ import ( "net/http" "net/url" "path" + "strconv" "strings" repo_model "code.gitea.io/gitea/models/repo" @@ -22,14 +23,17 @@ func goGet(ctx *context.Context) { return } - parts := strings.SplitN(ctx.Req.URL.EscapedPath(), "/", 4) + parts := strings.SplitN(ctx.Req.URL.EscapedPath(), "/", 5) if len(parts) < 3 { return } - + var group string ownerName := parts[1] repoName := parts[2] + if len(parts) > 3 { + group = parts[3] + } // Quick responses appropriate go-get meta with status 200 // regardless of if user have access to the repository, @@ -56,7 +60,11 @@ func goGet(ctx *context.Context) { if err == nil && len(repo.DefaultBranch) > 0 { branchName = repo.DefaultBranch } - prefix := setting.AppURL + path.Join(url.PathEscape(ownerName), url.PathEscape(repoName), "src", "branch", util.PathEscapeSegments(branchName)) + prefix := setting.AppURL + url.PathEscape(ownerName) + if group != "" { + prefix = path.Join(prefix, group) + } + prefix = path.Join(prefix, url.PathEscape(repoName), "src", "branch", util.PathEscapeSegments(branchName)) appURL, _ := url.Parse(setting.AppURL) @@ -68,10 +76,11 @@ func goGet(ctx *context.Context) { goGetImport := context.ComposeGoGetImport(ctx, ownerName, trimmedRepoName) var cloneURL string + gid, _ := strconv.ParseInt(group, 10, 64) if setting.Repository.GoGetCloneURLProtocol == "ssh" { - cloneURL = repo_model.ComposeSSHCloneURL(ctx.Doer, ownerName, repoName) + cloneURL = repo_model.ComposeSSHCloneURL(ctx.Doer, ownerName, repoName, gid) } else { - cloneURL = repo_model.ComposeHTTPSCloneURL(ctx, ownerName, repoName) + cloneURL = repo_model.ComposeHTTPSCloneURL(ctx, ownerName, repoName, gid) } goImportContent := fmt.Sprintf("%s git %s", goGetImport, cloneURL /*CloneLink*/) goSourceContent := fmt.Sprintf("%s _ %s %s", goGetImport, prefix+"{/dir}" /*GoDocDirectory*/, prefix+"{/dir}/{file}#L{line}" /*GoDocFile*/) diff --git a/routers/web/user/setting/adopt.go b/routers/web/user/setting/adopt.go index abf9d8c6db..98b6b5912b 100644 --- a/routers/web/user/setting/adopt.go +++ b/routers/web/user/setting/adopt.go @@ -4,6 +4,9 @@ package setting import ( + "strconv" + "strings" + repo_model "code.gitea.io/gitea/models/repo" "code.gitea.io/gitea/modules/gitrepo" "code.gitea.io/gitea/modules/setting" @@ -21,18 +24,24 @@ func AdoptOrDeleteRepository(ctx *context.Context) { ctx.Data["allowDelete"] = allowDelete dir := ctx.FormString("id") + var gid int64 + if len(strings.Split(dir, "/")) > 1 { + split := strings.Split(dir, "/") + dir = split[0] + gid, _ = strconv.ParseInt(split[1], 10, 64) + } action := ctx.FormString("action") ctxUser := ctx.Doer // check not a repo - has, err := repo_model.IsRepositoryModelExist(ctx, ctxUser, dir) + has, err := repo_model.IsRepositoryModelExist(ctx, ctxUser, dir, 0) if err != nil { ctx.ServerError("IsRepositoryExist", err) return } - exist, err := gitrepo.IsRepositoryExist(ctx, repo_model.StorageRepo(repo_model.RelativePath(ctxUser.Name, dir))) + exist, err := gitrepo.IsRepositoryExist(ctx, repo_model.StorageRepo(repo_model.RelativePath(ctxUser.Name, dir, gid))) if err != nil { ctx.ServerError("IsDir", err) return @@ -49,7 +58,7 @@ func AdoptOrDeleteRepository(ctx *context.Context) { } ctx.Flash.Success(ctx.Tr("repo.adopt_preexisting_success", dir)) } else if action == "delete" && allowDelete { - if err := repo_service.DeleteUnadoptedRepository(ctx, ctxUser, ctxUser, dir); err != nil { + if err := repo_service.DeleteUnadoptedRepository(ctx, ctxUser, ctxUser, dir, gid); err != nil { ctx.ServerError("repository.AdoptRepository", err) return } diff --git a/routers/web/web.go b/routers/web/web.go index e3dcf27cc4..49abcd2fe4 100644 --- a/routers/web/web.go +++ b/routers/web/web.go @@ -8,6 +8,7 @@ import ( "strings" auth_model "code.gitea.io/gitea/models/auth" + "code.gitea.io/gitea/models/db" "code.gitea.io/gitea/models/perm" "code.gitea.io/gitea/models/unit" "code.gitea.io/gitea/modules/git" diff --git a/services/context/repo.go b/services/context/repo.go index 3155ad3de9..c247d905dc 100644 --- a/services/context/repo.go +++ b/services/context/repo.go @@ -12,6 +12,7 @@ import ( "net/http" "net/url" "path" + "strconv" "strings" asymkey_model "code.gitea.io/gitea/models/asymkey" @@ -369,6 +370,7 @@ func ComposeGoGetImport(ctx context.Context, owner, repo string) string { func EarlyResponseForGoGetMeta(ctx *Context) { username := ctx.PathParam("username") reponame := strings.TrimSuffix(ctx.PathParam("reponame"), ".git") + groupID := ctx.PathParamInt64("group_id") if username == "" || reponame == "" { ctx.PlainText(http.StatusBadRequest, "invalid repository path") return @@ -376,9 +378,9 @@ func EarlyResponseForGoGetMeta(ctx *Context) { var cloneURL string if setting.Repository.GoGetCloneURLProtocol == "ssh" { - cloneURL = repo_model.ComposeSSHCloneURL(ctx.Doer, username, reponame) + cloneURL = repo_model.ComposeSSHCloneURL(ctx.Doer, username, reponame, groupID) } else { - cloneURL = repo_model.ComposeHTTPSCloneURL(ctx, username, reponame) + cloneURL = repo_model.ComposeHTTPSCloneURL(ctx, username, reponame, groupID) } goImportContent := fmt.Sprintf("%s git %s", ComposeGoGetImport(ctx, username, reponame), cloneURL) htmlMeta := fmt.Sprintf(``, html.EscapeString(goImportContent)) @@ -468,6 +470,20 @@ func RepoAssignment(ctx *Context) { var err error userName := ctx.PathParam("username") repoName := ctx.PathParam("reponame") + group := ctx.PathParam("group_id") + var gid int64 + if group != "" { + gid, _ = strconv.ParseInt(group, 10, 64) + if gid == 0 { + q := ctx.Req.URL.RawQuery + if q != "" { + q = "?" + q + } + ctx.Redirect(strings.Replace(ctx.Link, "/0/", "/", 1) + q) + return + } + group += "/" + } repoName = strings.TrimSuffix(repoName, ".git") if setting.Other.EnableFeed { ctx.Data["EnableFeed"] = true @@ -514,7 +530,7 @@ func RepoAssignment(ctx *Context) { redirectRepoName += originalRepoName[len(redirectRepoName)+5:] redirectPath := strings.Replace( ctx.Req.URL.EscapedPath(), - url.PathEscape(userName)+"/"+url.PathEscape(originalRepoName), + url.PathEscape(userName)+"/"+group+url.PathEscape(originalRepoName), url.PathEscape(userName)+"/"+url.PathEscape(redirectRepoName)+"/wiki", 1, ) @@ -546,6 +562,9 @@ func RepoAssignment(ctx *Context) { } return } + if repo.GroupID != gid { + ctx.NotFound(nil) + } repo.Owner = ctx.Repo.Owner repoAssignment(ctx, repo) diff --git a/services/repository/adopt.go b/services/repository/adopt.go index 1255967e59..887d33e3ec 100644 --- a/services/repository/adopt.go +++ b/services/repository/adopt.go @@ -208,12 +208,12 @@ func adoptRepository(ctx context.Context, repo *repo_model.Repository, defaultBr } // DeleteUnadoptedRepository deletes unadopted repository files from the filesystem -func DeleteUnadoptedRepository(ctx context.Context, doer, u *user_model.User, repoName string) error { +func DeleteUnadoptedRepository(ctx context.Context, doer, u *user_model.User, repoName string, groupID int64) error { if err := repo_model.IsUsableRepoName(repoName); err != nil { return err } - relativePath := repo_model.RelativePath(u.Name, repoName) + relativePath := repo_model.RelativePath(u.Name, repoName, groupID) exist, err := gitrepo.IsRepositoryExist(ctx, repo_model.StorageRepo(relativePath)) if err != nil { log.Error("Unable to check if %s exists. Error: %v", relativePath, err) @@ -226,7 +226,7 @@ func DeleteUnadoptedRepository(ctx context.Context, doer, u *user_model.User, re } } - if exist, err := repo_model.IsRepositoryModelExist(ctx, u, repoName); err != nil { + if exist, err := repo_model.IsRepositoryModelExist(ctx, u, repoName, groupID); err != nil { return err } else if exist { return repo_model.ErrRepoAlreadyExist{ diff --git a/services/repository/create.go b/services/repository/create.go index a6686595e6..3a0397350c 100644 --- a/services/repository/create.go +++ b/services/repository/create.go @@ -366,7 +366,7 @@ func createRepositoryInDB(ctx context.Context, doer, u *user_model.User, repo *r return err } - has, err := repo_model.IsRepositoryModelExist(ctx, u, repo.Name) + has, err := repo_model.IsRepositoryModelExist(ctx, u, repo.Name, repo.GroupID) if err != nil { return fmt.Errorf("IsRepositoryExist: %w", err) } else if has { diff --git a/services/repository/transfer.go b/services/repository/transfer.go index 217ecb0124..76cae7ded3 100644 --- a/services/repository/transfer.go +++ b/services/repository/transfer.go @@ -91,12 +91,12 @@ func AcceptTransferOwnership(ctx context.Context, repo *repo_model.Repository, d } // isRepositoryModelOrDirExist returns true if the repository with given name under user has already existed. -func isRepositoryModelOrDirExist(ctx context.Context, u *user_model.User, repoName string) (bool, error) { - has, err := repo_model.IsRepositoryModelExist(ctx, u, repoName) +func isRepositoryModelOrDirExist(ctx context.Context, u *user_model.User, repoName string, groupID int64) (bool, error) { + has, err := repo_model.IsRepositoryModelExist(ctx, u, repoName, groupID) if err != nil { return false, err } - repo := repo_model.StorageRepo(repo_model.RelativePath(u.Name, repoName)) + repo := repo_model.StorageRepo(repo_model.RelativePath(u.Name, repoName, groupID)) isExist, err := gitrepo.IsRepositoryExist(ctx, repo) return has || isExist, err } @@ -118,7 +118,7 @@ func transferOwnership(ctx context.Context, doer *user_model.User, newOwnerName } if repoRenamed { - oldRelativePath, newRelativePath := repo_model.RelativePath(newOwnerName, repo.Name), repo_model.RelativePath(oldOwnerName, repo.Name) + oldRelativePath, newRelativePath := repo_model.RelativePath(newOwnerName, repo.Name, 0), repo_model.RelativePath(oldOwnerName, repo.Name, repo.GroupID) if err := gitrepo.RenameRepository(ctx, repo_model.StorageRepo(oldRelativePath), repo_model.StorageRepo(newRelativePath)); err != nil { log.Critical("Unable to move repository %s/%s directory from %s back to correct place %s: %v", oldOwnerName, repo.Name, oldRelativePath, newRelativePath, err) @@ -126,7 +126,7 @@ func transferOwnership(ctx context.Context, doer *user_model.User, newOwnerName } if wikiRenamed { - oldRelativePath, newRelativePath := repo_model.RelativeWikiPath(newOwnerName, repo.Name), repo_model.RelativeWikiPath(oldOwnerName, repo.Name) + oldRelativePath, newRelativePath := repo_model.RelativeWikiPath(newOwnerName, repo.Name, 0), repo_model.RelativeWikiPath(oldOwnerName, repo.Name, repo.GroupID) if err := gitrepo.RenameRepository(ctx, repo_model.StorageRepo(oldRelativePath), repo_model.StorageRepo(newRelativePath)); err != nil { log.Critical("Unable to move wiki for repository %s/%s directory from %s back to correct place %s: %v", oldOwnerName, repo.Name, oldRelativePath, newRelativePath, err) @@ -154,7 +154,7 @@ func transferOwnership(ctx context.Context, doer *user_model.User, newOwnerName newOwnerName = newOwner.Name // ensure capitalisation matches // Check if new owner has repository with same name. - if has, err := isRepositoryModelOrDirExist(ctx, newOwner, repo.Name); err != nil { + if has, err := isRepositoryModelOrDirExist(ctx, newOwner, repo.Name, 0); err != nil { return fmt.Errorf("IsRepositoryExist: %w", err) } else if has { return repo_model.ErrRepoAlreadyExist{ @@ -304,19 +304,19 @@ func transferOwnership(ctx context.Context, doer *user_model.User, newOwnerName } // Rename remote repository to new path and delete local copy. - oldRelativePath, newRelativePath := repo_model.RelativePath(oldOwner.Name, repo.Name), repo_model.RelativePath(newOwner.Name, repo.Name) + oldRelativePath, newRelativePath := repo_model.RelativePath(oldOwner.Name, repo.Name, repo.GroupID), repo_model.RelativePath(newOwner.Name, repo.Name, repo.GroupID) if err := gitrepo.RenameRepository(ctx, repo_model.StorageRepo(oldRelativePath), repo_model.StorageRepo(newRelativePath)); err != nil { return fmt.Errorf("rename repository directory: %w", err) } repoRenamed = true // Rename remote wiki repository to new path and delete local copy. - wikiStorageRepo := repo_model.StorageRepo(repo_model.RelativeWikiPath(oldOwner.Name, repo.Name)) + wikiStorageRepo := repo_model.StorageRepo(repo_model.RelativeWikiPath(oldOwner.Name, repo.Name, repo.GroupID)) if isExist, err := gitrepo.IsRepositoryExist(ctx, wikiStorageRepo); err != nil { log.Error("Unable to check if %s exists. Error: %v", wikiStorageRepo.RelativePath(), err) return err } else if isExist { - if err := gitrepo.RenameRepository(ctx, wikiStorageRepo, repo_model.StorageRepo(repo_model.RelativeWikiPath(newOwner.Name, repo.Name))); err != nil { + if err := gitrepo.RenameRepository(ctx, wikiStorageRepo, repo_model.StorageRepo(repo_model.RelativeWikiPath(newOwner.Name, repo.Name, repo.GroupID))); err != nil { return fmt.Errorf("rename repository wiki: %w", err) } wikiRenamed = true @@ -365,7 +365,7 @@ func changeRepositoryName(ctx context.Context, repo *repo_model.Repository, newR return err } - has, err := isRepositoryModelOrDirExist(ctx, repo.Owner, newRepoName) + has, err := isRepositoryModelOrDirExist(ctx, repo.Owner, newRepoName, repo.GroupID) if err != nil { return fmt.Errorf("IsRepositoryExist: %w", err) } else if has { @@ -376,13 +376,13 @@ func changeRepositoryName(ctx context.Context, repo *repo_model.Repository, newR } if err = gitrepo.RenameRepository(ctx, repo, - repo_model.StorageRepo(repo_model.RelativePath(repo.OwnerName, newRepoName))); err != nil { + repo_model.StorageRepo(repo_model.RelativePath(repo.OwnerName, newRepoName, 0))); err != nil { return fmt.Errorf("rename repository directory: %w", err) } if HasWiki(ctx, repo) { if err = gitrepo.RenameRepository(ctx, repo.WikiStorageRepo(), repo_model.StorageRepo( - repo_model.RelativeWikiPath(repo.OwnerName, newRepoName))); err != nil { + repo_model.RelativeWikiPath(repo.OwnerName, newRepoName, repo.GroupID))); err != nil { return fmt.Errorf("rename repository wiki: %w", err) } }