diff options
Diffstat (limited to 'routers')
30 files changed, 1001 insertions, 1566 deletions
diff --git a/routers/api/packages/api.go b/routers/api/packages/api.go index df5897e45e..f65c4b99ff 100644 --- a/routers/api/packages/api.go +++ b/routers/api/packages/api.go @@ -5,8 +5,6 @@ package packages import ( "net/http" - "regexp" - "strings" auth_model "code.gitea.io/gitea/models/auth" "code.gitea.io/gitea/models/perm" @@ -282,42 +280,10 @@ func CommonRoutes() *web.Router { }) }) }, reqPackageAccess(perm.AccessModeRead)) - r.Group("/conda", func() { - var ( - downloadPattern = regexp.MustCompile(`\A(.+/)?(.+)/((?:[^/]+(?:\.tar\.bz2|\.conda))|(?:current_)?repodata\.json(?:\.bz2)?)\z`) - uploadPattern = regexp.MustCompile(`\A(.+/)?([^/]+(?:\.tar\.bz2|\.conda))\z`) - ) - - r.Get("/*", func(ctx *context.Context) { - m := downloadPattern.FindStringSubmatch(ctx.PathParam("*")) - if len(m) == 0 { - ctx.Status(http.StatusNotFound) - return - } - - ctx.SetPathParam("channel", strings.TrimSuffix(m[1], "/")) - ctx.SetPathParam("architecture", m[2]) - ctx.SetPathParam("filename", m[3]) - - switch m[3] { - case "repodata.json", "repodata.json.bz2", "current_repodata.json", "current_repodata.json.bz2": - conda.EnumeratePackages(ctx) - default: - conda.DownloadPackageFile(ctx) - } - }) - r.Put("/*", reqPackageAccess(perm.AccessModeWrite), func(ctx *context.Context) { - m := uploadPattern.FindStringSubmatch(ctx.PathParam("*")) - if len(m) == 0 { - ctx.Status(http.StatusNotFound) - return - } - - ctx.SetPathParam("channel", strings.TrimSuffix(m[1], "/")) - ctx.SetPathParam("filename", m[2]) - - conda.UploadPackageFile(ctx) - }) + r.PathGroup("/conda/*", func(g *web.RouterPathGroup) { + g.MatchPath("GET", "/<architecture>/<filename>", conda.ListOrGetPackages) + g.MatchPath("GET", "/<channel:*>/<architecture>/<filename>", conda.ListOrGetPackages) + g.MatchPath("PUT", "/<channel:*>/<filename>", reqPackageAccess(perm.AccessModeWrite), conda.UploadPackageFile) }, reqPackageAccess(perm.AccessModeRead)) r.Group("/cran", func() { r.Group("/src", func() { @@ -358,60 +324,15 @@ func CommonRoutes() *web.Router { }, reqPackageAccess(perm.AccessModeRead)) r.Group("/go", func() { r.Put("/upload", reqPackageAccess(perm.AccessModeWrite), goproxy.UploadPackage) - r.Get("/sumdb/sum.golang.org/supported", func(ctx *context.Context) { - ctx.Status(http.StatusNotFound) - }) + r.Get("/sumdb/sum.golang.org/supported", http.NotFound) - // Manual mapping of routes because the package name contains slashes which chi does not support // https://go.dev/ref/mod#goproxy-protocol - r.Get("/*", func(ctx *context.Context) { - path := ctx.PathParam("*") - - if strings.HasSuffix(path, "/@latest") { - ctx.SetPathParam("name", path[:len(path)-len("/@latest")]) - ctx.SetPathParam("version", "latest") - - goproxy.PackageVersionMetadata(ctx) - return - } - - parts := strings.SplitN(path, "/@v/", 2) - if len(parts) != 2 { - ctx.Status(http.StatusNotFound) - return - } - - ctx.SetPathParam("name", parts[0]) - - // <package/name>/@v/list - if parts[1] == "list" { - goproxy.EnumeratePackageVersions(ctx) - return - } - - // <package/name>/@v/<version>.zip - if strings.HasSuffix(parts[1], ".zip") { - ctx.SetPathParam("version", parts[1][:len(parts[1])-len(".zip")]) - - goproxy.DownloadPackageFile(ctx) - return - } - // <package/name>/@v/<version>.info - if strings.HasSuffix(parts[1], ".info") { - ctx.SetPathParam("version", parts[1][:len(parts[1])-len(".info")]) - - goproxy.PackageVersionMetadata(ctx) - return - } - // <package/name>/@v/<version>.mod - if strings.HasSuffix(parts[1], ".mod") { - ctx.SetPathParam("version", parts[1][:len(parts[1])-len(".mod")]) - - goproxy.PackageVersionGoModContent(ctx) - return - } - - ctx.Status(http.StatusNotFound) + r.PathGroup("/*", func(g *web.RouterPathGroup) { + g.MatchPath("GET", "/<name:*>/@<version:latest>", goproxy.PackageVersionMetadata) + g.MatchPath("GET", "/<name:*>/@v/list", goproxy.EnumeratePackageVersions) + g.MatchPath("GET", "/<name:*>/@v/<version>.zip", goproxy.DownloadPackageFile) + g.MatchPath("GET", "/<name:*>/@v/<version>.info", goproxy.PackageVersionMetadata) + g.MatchPath("GET", "/<name:*>/@v/<version>.mod", goproxy.PackageVersionGoModContent) }) }, reqPackageAccess(perm.AccessModeRead)) r.Group("/generic", func() { @@ -532,82 +453,24 @@ func CommonRoutes() *web.Router { }) }) }, reqPackageAccess(perm.AccessModeRead)) + r.Group("/pypi", func() { r.Post("/", reqPackageAccess(perm.AccessModeWrite), pypi.UploadPackageFile) r.Get("/files/{id}/{version}/{filename}", pypi.DownloadPackageFile) r.Get("/simple/{id}", pypi.PackageMetadata) }, reqPackageAccess(perm.AccessModeRead)) - r.Group("/rpm", func() { - r.Group("/repository.key", func() { - r.Head("", rpm.GetRepositoryKey) - r.Get("", rpm.GetRepositoryKey) - }) - var ( - repoPattern = regexp.MustCompile(`\A(.*?)\.repo\z`) - uploadPattern = regexp.MustCompile(`\A(.*?)/upload\z`) - filePattern = regexp.MustCompile(`\A(.*?)/package/([^/]+)/([^/]+)/([^/]+)(?:/([^/]+\.rpm)|)\z`) - repoFilePattern = regexp.MustCompile(`\A(.*?)/repodata/([^/]+)\z`) - ) - - r.Methods("HEAD,GET,PUT,DELETE", "*", func(ctx *context.Context) { - path := ctx.PathParam("*") - isHead := ctx.Req.Method == http.MethodHead - isGetHead := ctx.Req.Method == http.MethodHead || ctx.Req.Method == http.MethodGet - isPut := ctx.Req.Method == http.MethodPut - isDelete := ctx.Req.Method == http.MethodDelete - - m := repoPattern.FindStringSubmatch(path) - if len(m) == 2 && isGetHead { - ctx.SetPathParam("group", strings.Trim(m[1], "/")) - rpm.GetRepositoryConfig(ctx) - return - } - - m = repoFilePattern.FindStringSubmatch(path) - if len(m) == 3 && isGetHead { - ctx.SetPathParam("group", strings.Trim(m[1], "/")) - ctx.SetPathParam("filename", m[2]) - if isHead { - rpm.CheckRepositoryFileExistence(ctx) - } else { - rpm.GetRepositoryFile(ctx) - } - return - } - - m = uploadPattern.FindStringSubmatch(path) - if len(m) == 2 && isPut { - reqPackageAccess(perm.AccessModeWrite)(ctx) - if ctx.Written() { - return - } - ctx.SetPathParam("group", strings.Trim(m[1], "/")) - rpm.UploadPackageFile(ctx) - return - } - - m = filePattern.FindStringSubmatch(path) - if len(m) == 6 && (isGetHead || isDelete) { - ctx.SetPathParam("group", strings.Trim(m[1], "/")) - ctx.SetPathParam("name", m[2]) - ctx.SetPathParam("version", m[3]) - ctx.SetPathParam("architecture", m[4]) - if isGetHead { - rpm.DownloadPackageFile(ctx) - } else { - reqPackageAccess(perm.AccessModeWrite)(ctx) - if ctx.Written() { - return - } - rpm.DeletePackageFile(ctx) - } - return - } - - ctx.Status(http.StatusNotFound) - }) + r.Methods("HEAD,GET", "/rpm.repo", reqPackageAccess(perm.AccessModeRead), rpm.GetRepositoryConfig) + r.PathGroup("/rpm/*", func(g *web.RouterPathGroup) { + g.MatchPath("HEAD,GET", "/repository.key", rpm.GetRepositoryKey) + g.MatchPath("HEAD,GET", "/<group:*>.repo", rpm.GetRepositoryConfig) + g.MatchPath("HEAD", "/<group:*>/repodata/<filename>", rpm.CheckRepositoryFileExistence) + g.MatchPath("GET", "/<group:*>/repodata/<filename>", rpm.GetRepositoryFile) + g.MatchPath("PUT", "/<group:*>/upload", reqPackageAccess(perm.AccessModeWrite), rpm.UploadPackageFile) + g.MatchPath("HEAD,GET", "/<group:*>/package/<name>/<version>/<architecture>", rpm.DownloadPackageFile) + g.MatchPath("DELETE", "/<group:*>/package/<name>/<version>/<architecture>", reqPackageAccess(perm.AccessModeWrite), rpm.DeletePackageFile) }, reqPackageAccess(perm.AccessModeRead)) + r.Group("/rubygems", func() { r.Get("/specs.4.8.gz", rubygems.EnumeratePackages) r.Get("/latest_specs.4.8.gz", rubygems.EnumeratePackagesLatest) @@ -621,6 +484,7 @@ func CommonRoutes() *web.Router { r.Delete("/yank", rubygems.DeletePackage) }, reqPackageAccess(perm.AccessModeWrite)) }, reqPackageAccess(perm.AccessModeRead)) + r.Group("/swift", func() { r.Group("", func() { // Needs to be unauthenticated. r.Post("", swift.CheckAuthenticate) @@ -632,31 +496,12 @@ func CommonRoutes() *web.Router { r.Get("", swift.EnumeratePackageVersions) r.Get(".json", swift.EnumeratePackageVersions) }, swift.CheckAcceptMediaType(swift.AcceptJSON)) - r.Group("/{version}", func() { - r.Get("/Package.swift", swift.CheckAcceptMediaType(swift.AcceptSwift), swift.DownloadManifest) - r.Put("", reqPackageAccess(perm.AccessModeWrite), swift.CheckAcceptMediaType(swift.AcceptJSON), swift.UploadPackageFile) - r.Get("", func(ctx *context.Context) { - // Can't use normal routes here: https://github.com/go-chi/chi/issues/781 - - version := ctx.PathParam("version") - if strings.HasSuffix(version, ".zip") { - swift.CheckAcceptMediaType(swift.AcceptZip)(ctx) - if ctx.Written() { - return - } - ctx.SetPathParam("version", version[:len(version)-4]) - swift.DownloadPackageFile(ctx) - } else { - swift.CheckAcceptMediaType(swift.AcceptJSON)(ctx) - if ctx.Written() { - return - } - if strings.HasSuffix(version, ".json") { - ctx.SetPathParam("version", version[:len(version)-5]) - } - swift.PackageVersionMetadata(ctx) - } - }) + r.PathGroup("/*", func(g *web.RouterPathGroup) { + g.MatchPath("GET", "/<version>.json", swift.CheckAcceptMediaType(swift.AcceptJSON), swift.PackageVersionMetadata) + g.MatchPath("GET", "/<version>.zip", swift.CheckAcceptMediaType(swift.AcceptZip), swift.DownloadPackageFile) + g.MatchPath("GET", "/<version>/Package.swift", swift.CheckAcceptMediaType(swift.AcceptSwift), swift.DownloadManifest) + g.MatchPath("GET", "/<version>", swift.CheckAcceptMediaType(swift.AcceptJSON), swift.PackageVersionMetadata) + g.MatchPath("PUT", "/<version>", reqPackageAccess(perm.AccessModeWrite), swift.CheckAcceptMediaType(swift.AcceptJSON), swift.UploadPackageFile) }) }) r.Get("/identifiers", swift.CheckAcceptMediaType(swift.AcceptJSON), swift.LookupPackageIdentifiers) @@ -705,18 +550,13 @@ func ContainerRoutes() *web.Router { r.PathGroup("/*", func(g *web.RouterPathGroup) { g.MatchPath("POST", "/<image:*>/blobs/uploads", reqPackageAccess(perm.AccessModeWrite), container.VerifyImageName, container.PostBlobsUploads) g.MatchPath("GET", "/<image:*>/tags/list", container.VerifyImageName, container.GetTagsList) - g.MatchPath("GET,PATCH,PUT,DELETE", `/<image:*>/blobs/uploads/<uuid:[-.=\w]+>`, reqPackageAccess(perm.AccessModeWrite), container.VerifyImageName, func(ctx *context.Context) { - switch ctx.Req.Method { - case http.MethodGet: - container.GetBlobsUpload(ctx) - case http.MethodPatch: - container.PatchBlobsUpload(ctx) - case http.MethodPut: - container.PutBlobsUpload(ctx) - default: /* DELETE */ - container.DeleteBlobsUpload(ctx) - } - }) + + patternBlobsUploadsUUID := g.PatternRegexp(`/<image:*>/blobs/uploads/<uuid:[-.=\w]+>`, reqPackageAccess(perm.AccessModeWrite), container.VerifyImageName) + g.MatchPattern("GET", patternBlobsUploadsUUID, container.GetBlobsUpload) + g.MatchPattern("PATCH", patternBlobsUploadsUUID, container.PatchBlobsUpload) + g.MatchPattern("PUT", patternBlobsUploadsUUID, container.PutBlobsUpload) + g.MatchPattern("DELETE", patternBlobsUploadsUUID, container.DeleteBlobsUpload) + g.MatchPath("HEAD", `/<image:*>/blobs/<digest>`, container.VerifyImageName, container.HeadBlob) g.MatchPath("GET", `/<image:*>/blobs/<digest>`, container.VerifyImageName, container.GetBlob) g.MatchPath("DELETE", `/<image:*>/blobs/<digest>`, container.VerifyImageName, reqPackageAccess(perm.AccessModeWrite), container.DeleteBlob) diff --git a/routers/api/packages/conda/conda.go b/routers/api/packages/conda/conda.go index fe7542dd18..cfe069d6db 100644 --- a/routers/api/packages/conda/conda.go +++ b/routers/api/packages/conda/conda.go @@ -36,6 +36,24 @@ func apiError(ctx *context.Context, status int, obj any) { }) } +func isCondaPackageFileName(filename string) bool { + return strings.HasSuffix(filename, ".tar.bz2") || strings.HasSuffix(filename, ".conda") +} + +func ListOrGetPackages(ctx *context.Context) { + filename := ctx.PathParam("filename") + switch filename { + case "repodata.json", "repodata.json.bz2", "current_repodata.json", "current_repodata.json.bz2": + EnumeratePackages(ctx) + return + } + if isCondaPackageFileName(filename) { + DownloadPackageFile(ctx) + return + } + ctx.NotFound(nil) +} + func EnumeratePackages(ctx *context.Context) { type Info struct { Subdir string `json:"subdir"` @@ -174,6 +192,12 @@ func EnumeratePackages(ctx *context.Context) { } func UploadPackageFile(ctx *context.Context) { + filename := ctx.PathParam("filename") + if !isCondaPackageFileName(filename) { + apiError(ctx, http.StatusBadRequest, nil) + return + } + upload, needToClose, err := ctx.UploadStream() if err != nil { apiError(ctx, http.StatusInternalServerError, err) @@ -191,7 +215,7 @@ func UploadPackageFile(ctx *context.Context) { defer buf.Close() var pck *conda_module.Package - if strings.HasSuffix(strings.ToLower(ctx.PathParam("filename")), ".tar.bz2") { + if strings.HasSuffix(filename, ".tar.bz2") { pck, err = conda_module.ParsePackageBZ2(buf) } else { pck, err = conda_module.ParsePackageConda(buf, buf.Size()) diff --git a/routers/api/packages/container/blob.go b/routers/api/packages/container/blob.go index 2ea9b3839c..abfc21f95a 100644 --- a/routers/api/packages/container/blob.go +++ b/routers/api/packages/container/blob.go @@ -90,14 +90,14 @@ func mountBlob(ctx context.Context, pi *packages_service.PackageInfo, pb *packag }) } -func containerPkgName(piOwnerID int64, piName string) string { - return fmt.Sprintf("pkg_%d_container_%s", piOwnerID, strings.ToLower(piName)) +func containerGlobalLockKey(piOwnerID int64, piName, usage string) string { + return fmt.Sprintf("pkg_%d_container_%s_%s", piOwnerID, strings.ToLower(piName), usage) } func getOrCreateUploadVersion(ctx context.Context, pi *packages_service.PackageInfo) (*packages_model.PackageVersion, error) { var uploadVersion *packages_model.PackageVersion - releaser, err := globallock.Lock(ctx, containerPkgName(pi.Owner.ID, pi.Name)) + releaser, err := globallock.Lock(ctx, containerGlobalLockKey(pi.Owner.ID, pi.Name, "package")) if err != nil { return nil, err } @@ -178,7 +178,7 @@ func createFileForBlob(ctx context.Context, pv *packages_model.PackageVersion, p } func deleteBlob(ctx context.Context, ownerID int64, image string, digest digest.Digest) error { - releaser, err := globallock.Lock(ctx, containerPkgName(ownerID, image)) + releaser, err := globallock.Lock(ctx, containerGlobalLockKey(ownerID, image, "blob")) if err != nil { return err } diff --git a/routers/api/packages/container/container.go b/routers/api/packages/container/container.go index d1b80daccf..aeec16be4b 100644 --- a/routers/api/packages/container/container.go +++ b/routers/api/packages/container/container.go @@ -32,7 +32,7 @@ import ( packages_service "code.gitea.io/gitea/services/packages" container_service "code.gitea.io/gitea/services/packages/container" - digest "github.com/opencontainers/go-digest" + "github.com/opencontainers/go-digest" ) // maximum size of a container manifest diff --git a/routers/api/packages/container/manifest.go b/routers/api/packages/container/manifest.go index b69b7af3f7..22ea11c8ce 100644 --- a/routers/api/packages/container/manifest.go +++ b/routers/api/packages/container/manifest.go @@ -16,6 +16,7 @@ import ( packages_model "code.gitea.io/gitea/models/packages" container_model "code.gitea.io/gitea/models/packages/container" user_model "code.gitea.io/gitea/models/user" + "code.gitea.io/gitea/modules/globallock" "code.gitea.io/gitea/modules/json" "code.gitea.io/gitea/modules/log" packages_module "code.gitea.io/gitea/modules/packages" @@ -61,6 +62,13 @@ func processManifest(ctx context.Context, mci *manifestCreationInfo, buf *packag } } + // .../container/manifest.go:453:createManifestBlob() [E] Error inserting package blob: Error 1062 (23000): Duplicate entry '..........' for key 'package_blob.UQE_package_blob_md5' + releaser, err := globallock.Lock(ctx, containerGlobalLockKey(mci.Owner.ID, mci.Image, "manifest")) + if err != nil { + return "", err + } + defer releaser() + if container_module.IsMediaTypeImageManifest(mci.MediaType) { return processOciImageManifest(ctx, mci, buf) } else if container_module.IsMediaTypeImageIndex(mci.MediaType) { diff --git a/routers/api/v1/repo/file.go b/routers/api/v1/repo/file.go index f40d39a251..1c7b57b922 100644 --- a/routers/api/v1/repo/file.go +++ b/routers/api/v1/repo/file.go @@ -470,6 +470,9 @@ func ChangeFiles(ctx *context.APIContext) { ctx.APIError(http.StatusUnprocessableEntity, err) return } + // FIXME: actually now we support more operations like "rename", "upload" + // FIXME: ChangeFileOperation.SHA is NOT required for update or delete if last commit is provided in the options. + // Need to fully fix them in API changeRepoFile := &files_service.ChangeRepoFile{ Operation: file.Operation, TreePath: file.Path, diff --git a/routers/install/install.go b/routers/install/install.go index b9bc41dfcf..c1da79454a 100644 --- a/routers/install/install.go +++ b/routers/install/install.go @@ -601,5 +601,7 @@ func SubmitInstall(ctx *context.Context) { // InstallDone shows the "post-install" page, makes it easier to develop the page. // The name is not called as "PostInstall" to avoid misinterpretation as a handler for "POST /install" func InstallDone(ctx *context.Context) { //nolint + hasUsers, _ := user_model.HasUsers(ctx) + ctx.Data["IsAccountCreated"] = hasUsers.HasAnyUser ctx.HTML(http.StatusOK, tplPostInstall) } diff --git a/routers/web/auth/auth.go b/routers/web/auth/auth.go index 87edbc357b..94f75f69ff 100644 --- a/routers/web/auth/auth.go +++ b/routers/web/auth/auth.go @@ -421,9 +421,11 @@ func SignOut(ctx *context.Context) { // SignUp render the register page func SignUp(ctx *context.Context) { ctx.Data["Title"] = ctx.Tr("sign_up") - ctx.Data["SignUpLink"] = setting.AppSubURL + "/user/sign_up" + hasUsers, _ := user_model.HasUsers(ctx) + ctx.Data["IsFirstTimeRegistration"] = !hasUsers.HasAnyUser + oauth2Providers, err := oauth2.GetOAuth2Providers(ctx, optional.Some(true)) if err != nil { ctx.ServerError("UserSignUp", err) @@ -610,7 +612,13 @@ func createUserInContext(ctx *context.Context, tpl templates.TplName, form any, // sends a confirmation email if required. func handleUserCreated(ctx *context.Context, u *user_model.User, gothUser *goth.User) (ok bool) { // Auto-set admin for the only user. - if user_model.CountUsers(ctx, nil) == 1 { + hasUsers, err := user_model.HasUsers(ctx) + if err != nil { + ctx.ServerError("HasUsers", err) + return false + } + if hasUsers.HasOnlyOneUser { + // the only user is the one just created, will set it as admin opts := &user_service.UpdateOptions{ IsActive: optional.Some(true), IsAdmin: user_service.UpdateOptionFieldFromValue(true), diff --git a/routers/web/explore/repo.go b/routers/web/explore/repo.go index 855c2a50db..f0d7d0ce7d 100644 --- a/routers/web/explore/repo.go +++ b/routers/web/explore/repo.go @@ -151,6 +151,7 @@ func Repos(ctx *context.Context) { ctx.Data["CodePageIsDisabled"] = setting.Service.Explore.DisableCodePage ctx.Data["Title"] = ctx.Tr("explore") ctx.Data["PageIsExplore"] = true + ctx.Data["ShowRepoOwnerOnList"] = true ctx.Data["PageIsExploreRepositories"] = true ctx.Data["IsRepoIndexerEnabled"] = setting.Indexer.RepoIndexerEnabled diff --git a/routers/web/org/setting.go b/routers/web/org/setting.go index 9dd0a98160..2bc1e8bc43 100644 --- a/routers/web/org/setting.go +++ b/routers/web/org/setting.go @@ -18,6 +18,7 @@ import ( repo_module "code.gitea.io/gitea/modules/repository" "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/templates" + "code.gitea.io/gitea/modules/util" "code.gitea.io/gitea/modules/web" shared_user "code.gitea.io/gitea/routers/web/shared/user" user_setting "code.gitea.io/gitea/routers/web/user/setting" @@ -31,8 +32,6 @@ import ( const ( // tplSettingsOptions template path for render settings tplSettingsOptions templates.TplName = "org/settings/options" - // tplSettingsDelete template path for render delete repository - tplSettingsDelete templates.TplName = "org/settings/delete" // tplSettingsHooks template path for render hook settings tplSettingsHooks templates.TplName = "org/settings/hooks" // tplSettingsLabels template path for render labels settings @@ -71,26 +70,6 @@ func SettingsPost(ctx *context.Context) { org := ctx.Org.Organization - if org.Name != form.Name { - if err := user_service.RenameUser(ctx, org.AsUser(), form.Name); err != nil { - if user_model.IsErrUserAlreadyExist(err) { - ctx.Data["Err_Name"] = true - ctx.RenderWithErr(ctx.Tr("form.username_been_taken"), tplSettingsOptions, &form) - } else if db.IsErrNameReserved(err) { - ctx.Data["Err_Name"] = true - ctx.RenderWithErr(ctx.Tr("repo.form.name_reserved", err.(db.ErrNameReserved).Name), tplSettingsOptions, &form) - } else if db.IsErrNamePatternNotAllowed(err) { - ctx.Data["Err_Name"] = true - ctx.RenderWithErr(ctx.Tr("repo.form.name_pattern_not_allowed", err.(db.ErrNamePatternNotAllowed).Pattern), tplSettingsOptions, &form) - } else { - ctx.ServerError("RenameUser", err) - } - return - } - - ctx.Org.OrgLink = setting.AppSubURL + "/org/" + url.PathEscape(org.Name) - } - if form.Email != "" { if err := user_service.ReplacePrimaryEmailAddress(ctx, org.AsUser(), form.Email); err != nil { ctx.Data["Err_Email"] = true @@ -163,42 +142,27 @@ func SettingsDeleteAvatar(ctx *context.Context) { ctx.JSONRedirect(ctx.Org.OrgLink + "/settings") } -// SettingsDelete response for deleting an organization -func SettingsDelete(ctx *context.Context) { - ctx.Data["Title"] = ctx.Tr("org.settings") - ctx.Data["PageIsOrgSettings"] = true - ctx.Data["PageIsSettingsDelete"] = true - - if ctx.Req.Method == http.MethodPost { - if ctx.Org.Organization.Name != ctx.FormString("org_name") { - ctx.Data["Err_OrgName"] = true - ctx.RenderWithErr(ctx.Tr("form.enterred_invalid_org_name"), tplSettingsDelete, nil) - return - } - - if err := org_service.DeleteOrganization(ctx, ctx.Org.Organization, false); err != nil { - if repo_model.IsErrUserOwnRepos(err) { - ctx.Flash.Error(ctx.Tr("form.org_still_own_repo")) - ctx.Redirect(ctx.Org.OrgLink + "/settings/delete") - } else if packages_model.IsErrUserOwnPackages(err) { - ctx.Flash.Error(ctx.Tr("form.org_still_own_packages")) - ctx.Redirect(ctx.Org.OrgLink + "/settings/delete") - } else { - ctx.ServerError("DeleteOrganization", err) - } - } else { - log.Trace("Organization deleted: %s", ctx.Org.Organization.Name) - ctx.Redirect(setting.AppSubURL + "/") - } +// SettingsDeleteOrgPost response for deleting an organization +func SettingsDeleteOrgPost(ctx *context.Context) { + if ctx.Org.Organization.Name != ctx.FormString("org_name") { + ctx.JSONError(ctx.Tr("form.enterred_invalid_org_name")) return } - if _, err := shared_user.RenderUserOrgHeader(ctx); err != nil { - ctx.ServerError("RenderUserOrgHeader", err) + if err := org_service.DeleteOrganization(ctx, ctx.Org.Organization, false /* no purge */); err != nil { + if repo_model.IsErrUserOwnRepos(err) { + ctx.JSONError(ctx.Tr("form.org_still_own_repo")) + } else if packages_model.IsErrUserOwnPackages(err) { + ctx.JSONError(ctx.Tr("form.org_still_own_packages")) + } else { + log.Error("DeleteOrganization: %v", err) + ctx.JSONError(util.Iif(ctx.Doer.IsAdmin, err.Error(), string(ctx.Tr("org.settings.delete_failed")))) + } return } - ctx.HTML(http.StatusOK, tplSettingsDelete) + ctx.Flash.Success(ctx.Tr("org.settings.delete_successful", ctx.Org.Organization.Name)) + ctx.JSONRedirect(setting.AppSubURL + "/") } // Webhooks render webhook list page @@ -250,3 +214,40 @@ func Labels(ctx *context.Context) { ctx.HTML(http.StatusOK, tplSettingsLabels) } + +// SettingsRenamePost response for renaming organization +func SettingsRenamePost(ctx *context.Context) { + form := web.GetForm(ctx).(*forms.RenameOrgForm) + if ctx.HasError() { + ctx.JSONError(ctx.GetErrMsg()) + return + } + + oldOrgName, newOrgName := ctx.Org.Organization.Name, form.NewOrgName + + if form.OrgName != oldOrgName { + ctx.JSONError(ctx.Tr("form.enterred_invalid_org_name")) + return + } + if newOrgName == oldOrgName { + ctx.JSONError(ctx.Tr("org.settings.rename_no_change")) + return + } + + if err := user_service.RenameUser(ctx, ctx.Org.Organization.AsUser(), newOrgName); err != nil { + if user_model.IsErrUserAlreadyExist(err) { + ctx.JSONError(ctx.Tr("org.form.name_been_taken", newOrgName)) + } else if db.IsErrNameReserved(err) { + ctx.JSONError(ctx.Tr("org.form.name_reserved", newOrgName)) + } else if db.IsErrNamePatternNotAllowed(err) { + ctx.JSONError(ctx.Tr("org.form.name_pattern_not_allowed", newOrgName)) + } else { + log.Error("RenameOrganization: %v", err) + ctx.JSONError(util.Iif(ctx.Doer.IsAdmin, err.Error(), string(ctx.Tr("org.settings.rename_failed")))) + } + return + } + + ctx.Flash.Success(ctx.Tr("org.settings.rename_success", oldOrgName, newOrgName)) + ctx.JSONRedirect(setting.AppSubURL + "/org/" + url.PathEscape(newOrgName) + "/settings") +} diff --git a/routers/web/repo/actions/actions.go b/routers/web/repo/actions/actions.go index 7f219811bd..202da407d2 100644 --- a/routers/web/repo/actions/actions.go +++ b/routers/web/repo/actions/actions.go @@ -318,7 +318,7 @@ func prepareWorkflowList(ctx *context.Context, workflows []Workflow) { ctx.Data["Page"] = pager ctx.Data["HasWorkflowsOrRuns"] = len(workflows) > 0 || len(runs) > 0 - ctx.Data["AllowDeleteWorkflowRuns"] = ctx.Repo.CanWrite(unit.TypeActions) + ctx.Data["CanWriteRepoUnitActions"] = ctx.Repo.CanWrite(unit.TypeActions) } // loadIsRefDeleted loads the IsRefDeleted field for each run in the list. diff --git a/routers/web/repo/cherry_pick.go b/routers/web/repo/cherry_pick.go deleted file mode 100644 index 690b830bc2..0000000000 --- a/routers/web/repo/cherry_pick.go +++ /dev/null @@ -1,193 +0,0 @@ -// Copyright 2021 The Gitea Authors. All rights reserved. -// SPDX-License-Identifier: MIT - -package repo - -import ( - "bytes" - "errors" - "net/http" - "strings" - - git_model "code.gitea.io/gitea/models/git" - "code.gitea.io/gitea/models/unit" - "code.gitea.io/gitea/modules/git" - "code.gitea.io/gitea/modules/setting" - "code.gitea.io/gitea/modules/templates" - "code.gitea.io/gitea/modules/util" - "code.gitea.io/gitea/modules/web" - "code.gitea.io/gitea/services/context" - "code.gitea.io/gitea/services/forms" - "code.gitea.io/gitea/services/repository/files" -) - -var tplCherryPick templates.TplName = "repo/editor/cherry_pick" - -// CherryPick handles cherrypick GETs -func CherryPick(ctx *context.Context) { - ctx.Data["SHA"] = ctx.PathParam("sha") - cherryPickCommit, err := ctx.Repo.GitRepo.GetCommit(ctx.PathParam("sha")) - if err != nil { - if git.IsErrNotExist(err) { - ctx.NotFound(err) - return - } - ctx.ServerError("GetCommit", err) - return - } - - if ctx.FormString("cherry-pick-type") == "revert" { - ctx.Data["CherryPickType"] = "revert" - ctx.Data["commit_summary"] = "revert " + ctx.PathParam("sha") - ctx.Data["commit_message"] = "revert " + cherryPickCommit.Message() - } else { - ctx.Data["CherryPickType"] = "cherry-pick" - splits := strings.SplitN(cherryPickCommit.Message(), "\n", 2) - ctx.Data["commit_summary"] = splits[0] - ctx.Data["commit_message"] = splits[1] - } - - canCommit := renderCommitRights(ctx) - ctx.Data["TreePath"] = "" - - if canCommit { - ctx.Data["commit_choice"] = frmCommitChoiceDirect - } else { - ctx.Data["commit_choice"] = frmCommitChoiceNewBranch - } - ctx.Data["new_branch_name"] = GetUniquePatchBranchName(ctx) - ctx.Data["last_commit"] = ctx.Repo.CommitID - ctx.Data["LineWrapExtensions"] = strings.Join(setting.Repository.Editor.LineWrapExtensions, ",") - ctx.Data["BranchLink"] = ctx.Repo.RepoLink + "/src/" + ctx.Repo.RefTypeNameSubURL() - - ctx.HTML(http.StatusOK, tplCherryPick) -} - -// CherryPickPost handles cherrypick POSTs -func CherryPickPost(ctx *context.Context) { - form := web.GetForm(ctx).(*forms.CherryPickForm) - - sha := ctx.PathParam("sha") - ctx.Data["SHA"] = sha - if form.Revert { - ctx.Data["CherryPickType"] = "revert" - } else { - ctx.Data["CherryPickType"] = "cherry-pick" - } - - canCommit := renderCommitRights(ctx) - branchName := ctx.Repo.BranchName - if form.CommitChoice == frmCommitChoiceNewBranch { - branchName = form.NewBranchName - } - ctx.Data["commit_summary"] = form.CommitSummary - ctx.Data["commit_message"] = form.CommitMessage - ctx.Data["commit_choice"] = form.CommitChoice - ctx.Data["new_branch_name"] = form.NewBranchName - ctx.Data["last_commit"] = ctx.Repo.CommitID - ctx.Data["LineWrapExtensions"] = strings.Join(setting.Repository.Editor.LineWrapExtensions, ",") - ctx.Data["BranchLink"] = ctx.Repo.RepoLink + "/src/" + ctx.Repo.RefTypeNameSubURL() - - if ctx.HasError() { - ctx.HTML(http.StatusOK, tplCherryPick) - return - } - - // Cannot commit to a an existing branch if user doesn't have rights - if branchName == ctx.Repo.BranchName && !canCommit { - ctx.Data["Err_NewBranchName"] = true - ctx.Data["commit_choice"] = frmCommitChoiceNewBranch - ctx.RenderWithErr(ctx.Tr("repo.editor.cannot_commit_to_protected_branch", branchName), tplCherryPick, &form) - return - } - - message := strings.TrimSpace(form.CommitSummary) - if message == "" { - if form.Revert { - message = ctx.Locale.TrString("repo.commit.revert-header", sha) - } else { - message = ctx.Locale.TrString("repo.commit.cherry-pick-header", sha) - } - } - - form.CommitMessage = strings.TrimSpace(form.CommitMessage) - if len(form.CommitMessage) > 0 { - message += "\n\n" + form.CommitMessage - } - - gitCommitter, valid := WebGitOperationGetCommitChosenEmailIdentity(ctx, form.CommitEmail) - if !valid { - ctx.Data["Err_CommitEmail"] = true - ctx.RenderWithErr(ctx.Tr("repo.editor.invalid_commit_email"), tplCherryPick, &form) - return - } - opts := &files.ApplyDiffPatchOptions{ - LastCommitID: form.LastCommit, - OldBranch: ctx.Repo.BranchName, - NewBranch: branchName, - Message: message, - Author: gitCommitter, - Committer: gitCommitter, - } - - // First lets try the simple plain read-tree -m approach - opts.Content = sha - if _, err := files.CherryPick(ctx, ctx.Repo.Repository, ctx.Doer, form.Revert, opts); err != nil { - if git_model.IsErrBranchAlreadyExists(err) { - // User has specified a branch that already exists - branchErr := err.(git_model.ErrBranchAlreadyExists) - ctx.Data["Err_NewBranchName"] = true - ctx.RenderWithErr(ctx.Tr("repo.editor.branch_already_exists", branchErr.BranchName), tplCherryPick, &form) - return - } else if files.IsErrCommitIDDoesNotMatch(err) { - ctx.RenderWithErr(ctx.Tr("repo.editor.file_changed_while_editing", ctx.Repo.RepoLink+"/compare/"+form.LastCommit+"..."+ctx.Repo.CommitID), tplPatchFile, &form) - return - } - // Drop through to the apply technique - - buf := &bytes.Buffer{} - if form.Revert { - if err := git.GetReverseRawDiff(ctx, ctx.Repo.Repository.RepoPath(), sha, buf); err != nil { - if git.IsErrNotExist(err) { - ctx.NotFound(errors.New("commit " + ctx.PathParam("sha") + " does not exist.")) - return - } - ctx.ServerError("GetRawDiff", err) - return - } - } else { - if err := git.GetRawDiff(ctx.Repo.GitRepo, sha, git.RawDiffType("patch"), buf); err != nil { - if git.IsErrNotExist(err) { - ctx.NotFound(errors.New("commit " + ctx.PathParam("sha") + " does not exist.")) - return - } - ctx.ServerError("GetRawDiff", err) - return - } - } - - opts.Content = buf.String() - ctx.Data["FileContent"] = opts.Content - - if _, err := files.ApplyDiffPatch(ctx, ctx.Repo.Repository, ctx.Doer, opts); err != nil { - if git_model.IsErrBranchAlreadyExists(err) { - // User has specified a branch that already exists - branchErr := err.(git_model.ErrBranchAlreadyExists) - ctx.Data["Err_NewBranchName"] = true - ctx.RenderWithErr(ctx.Tr("repo.editor.branch_already_exists", branchErr.BranchName), tplCherryPick, &form) - return - } else if files.IsErrCommitIDDoesNotMatch(err) { - ctx.RenderWithErr(ctx.Tr("repo.editor.file_changed_while_editing", ctx.Repo.RepoLink+"/compare/"+form.LastCommit+"..."+ctx.Repo.CommitID), tplPatchFile, &form) - return - } - ctx.RenderWithErr(ctx.Tr("repo.editor.fail_to_apply_patch", err), tplPatchFile, &form) - return - } - } - - if form.CommitChoice == frmCommitChoiceNewBranch && ctx.Repo.Repository.UnitEnabled(ctx, unit.TypePullRequests) { - ctx.Redirect(ctx.Repo.RepoLink + "/compare/" + util.PathEscapeSegments(ctx.Repo.BranchName) + "..." + util.PathEscapeSegments(form.NewBranchName)) - } else { - ctx.Redirect(ctx.Repo.RepoLink + "/src/branch/" + util.PathEscapeSegments(branchName)) - } -} diff --git a/routers/web/repo/editor.go b/routers/web/repo/editor.go index 1a090c9437..ae0b74b019 100644 --- a/routers/web/repo/editor.go +++ b/routers/web/repo/editor.go @@ -4,6 +4,7 @@ package repo import ( + "bytes" "fmt" "io" "net/http" @@ -11,18 +12,16 @@ import ( "strings" git_model "code.gitea.io/gitea/models/git" - repo_model "code.gitea.io/gitea/models/repo" "code.gitea.io/gitea/models/unit" "code.gitea.io/gitea/modules/charset" "code.gitea.io/gitea/modules/git" - "code.gitea.io/gitea/modules/json" + "code.gitea.io/gitea/modules/httplib" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/markup" "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/templates" "code.gitea.io/gitea/modules/util" "code.gitea.io/gitea/modules/web" - "code.gitea.io/gitea/routers/utils" "code.gitea.io/gitea/services/context" "code.gitea.io/gitea/services/context/upload" "code.gitea.io/gitea/services/forms" @@ -34,141 +33,261 @@ const ( tplEditDiffPreview templates.TplName = "repo/editor/diff_preview" tplDeleteFile templates.TplName = "repo/editor/delete" tplUploadFile templates.TplName = "repo/editor/upload" + tplPatchFile templates.TplName = "repo/editor/patch" + tplCherryPick templates.TplName = "repo/editor/cherry_pick" - frmCommitChoiceDirect string = "direct" - frmCommitChoiceNewBranch string = "commit-to-new-branch" + editorCommitChoiceDirect string = "direct" + editorCommitChoiceNewBranch string = "commit-to-new-branch" ) -func canCreateBasePullRequest(ctx *context.Context) bool { - baseRepo := ctx.Repo.Repository.BaseRepo - return baseRepo != nil && baseRepo.UnitEnabled(ctx, unit.TypePullRequests) +func prepareEditorCommitFormOptions(ctx *context.Context, editorAction string) *context.CommitFormOptions { + cleanedTreePath := files_service.CleanGitTreePath(ctx.Repo.TreePath) + if cleanedTreePath != ctx.Repo.TreePath { + redirectTo := fmt.Sprintf("%s/%s/%s/%s", ctx.Repo.RepoLink, editorAction, util.PathEscapeSegments(ctx.Repo.BranchName), util.PathEscapeSegments(cleanedTreePath)) + if ctx.Req.URL.RawQuery != "" { + redirectTo += "?" + ctx.Req.URL.RawQuery + } + ctx.Redirect(redirectTo) + return nil + } + + commitFormOptions, err := context.PrepareCommitFormOptions(ctx, ctx.Doer, ctx.Repo.Repository, ctx.Repo.Permission, ctx.Repo.RefFullName) + if err != nil { + ctx.ServerError("PrepareCommitFormOptions", err) + return nil + } + + if commitFormOptions.NeedFork { + ForkToEdit(ctx) + return nil + } + + if commitFormOptions.WillSubmitToFork && !commitFormOptions.TargetRepo.CanEnableEditor() { + ctx.Data["NotFoundPrompt"] = ctx.Locale.Tr("repo.editor.fork_not_editable") + ctx.NotFound(nil) + } + + ctx.Data["BranchLink"] = ctx.Repo.RepoLink + "/src/" + ctx.Repo.RefTypeNameSubURL() + ctx.Data["TreePath"] = ctx.Repo.TreePath + ctx.Data["CommitFormOptions"] = commitFormOptions + + // for online editor + ctx.Data["PreviewableExtensions"] = strings.Join(markup.PreviewableExtensions(), ",") + ctx.Data["LineWrapExtensions"] = strings.Join(setting.Repository.Editor.LineWrapExtensions, ",") + ctx.Data["IsEditingFileOnly"] = ctx.FormString("return_uri") != "" + ctx.Data["ReturnURI"] = ctx.FormString("return_uri") + + // form fields + ctx.Data["commit_summary"] = "" + ctx.Data["commit_message"] = "" + ctx.Data["commit_choice"] = util.Iif(commitFormOptions.CanCommitToBranch, editorCommitChoiceDirect, editorCommitChoiceNewBranch) + ctx.Data["new_branch_name"] = getUniquePatchBranchName(ctx, ctx.Doer.LowerName, commitFormOptions.TargetRepo) + ctx.Data["last_commit"] = ctx.Repo.CommitID + return commitFormOptions +} + +func prepareTreePathFieldsAndPaths(ctx *context.Context, treePath string) { + // show the tree path fields in the "breadcrumb" and help users to edit the target tree path + ctx.Data["TreeNames"], ctx.Data["TreePaths"] = getParentTreeFields(strings.TrimPrefix(treePath, "/")) +} + +type preparedEditorCommitForm[T any] struct { + form T + commonForm *forms.CommitCommonForm + CommitFormOptions *context.CommitFormOptions + OldBranchName string + NewBranchName string + GitCommitter *files_service.IdentityOptions } -func renderCommitRights(ctx *context.Context) bool { - canCommitToBranch, err := ctx.Repo.CanCommitToBranch(ctx, ctx.Doer) +func (f *preparedEditorCommitForm[T]) GetCommitMessage(defaultCommitMessage string) string { + commitMessage := util.IfZero(strings.TrimSpace(f.commonForm.CommitSummary), defaultCommitMessage) + if body := strings.TrimSpace(f.commonForm.CommitMessage); body != "" { + commitMessage += "\n\n" + body + } + return commitMessage +} + +func prepareEditorCommitSubmittedForm[T forms.CommitCommonFormInterface](ctx *context.Context) *preparedEditorCommitForm[T] { + form := web.GetForm(ctx).(T) + if ctx.HasError() { + ctx.JSONError(ctx.GetErrMsg()) + return nil + } + + commonForm := form.GetCommitCommonForm() + commonForm.TreePath = files_service.CleanGitTreePath(commonForm.TreePath) + + commitFormOptions, err := context.PrepareCommitFormOptions(ctx, ctx.Doer, ctx.Repo.Repository, ctx.Repo.Permission, ctx.Repo.RefFullName) if err != nil { - log.Error("CanCommitToBranch: %v", err) + ctx.ServerError("PrepareCommitFormOptions", err) + return nil + } + if commitFormOptions.NeedFork { + // It shouldn't happen, because we should have done the checks in the "GET" request. But just in case. + ctx.JSONError(ctx.Locale.TrString("error.not_found")) + return nil + } + + // check commit behavior + fromBaseBranch := ctx.FormString("from_base_branch") + commitToNewBranch := commonForm.CommitChoice == editorCommitChoiceNewBranch || fromBaseBranch != "" + targetBranchName := util.Iif(commitToNewBranch, commonForm.NewBranchName, ctx.Repo.BranchName) + if targetBranchName == ctx.Repo.BranchName && !commitFormOptions.CanCommitToBranch { + ctx.JSONError(ctx.Tr("repo.editor.cannot_commit_to_protected_branch", targetBranchName)) + return nil } - ctx.Data["CanCommitToBranch"] = canCommitToBranch - ctx.Data["CanCreatePullRequest"] = ctx.Repo.Repository.UnitEnabled(ctx, unit.TypePullRequests) || canCreateBasePullRequest(ctx) - return canCommitToBranch.CanCommitToBranch + // Committer user info + gitCommitter, valid := WebGitOperationGetCommitChosenEmailIdentity(ctx, commonForm.CommitEmail) + if !valid { + ctx.JSONError(ctx.Tr("repo.editor.invalid_commit_email")) + return nil + } + + if commitToNewBranch { + // if target branch exists, we should stop + targetBranchExists, err := git_model.IsBranchExist(ctx, commitFormOptions.TargetRepo.ID, targetBranchName) + if err != nil { + ctx.ServerError("IsBranchExist", err) + return nil + } else if targetBranchExists { + if fromBaseBranch != "" { + ctx.JSONError(ctx.Tr("repo.editor.fork_branch_exists", targetBranchName)) + } else { + ctx.JSONError(ctx.Tr("repo.editor.branch_already_exists", targetBranchName)) + } + return nil + } + } + + oldBranchName := ctx.Repo.BranchName + if fromBaseBranch != "" { + err = editorPushBranchToForkedRepository(ctx, ctx.Doer, ctx.Repo.Repository.BaseRepo, fromBaseBranch, commitFormOptions.TargetRepo, targetBranchName) + if err != nil { + log.Error("Unable to editorPushBranchToForkedRepository: %v", err) + ctx.JSONError(ctx.Tr("repo.editor.fork_failed_to_push_branch", targetBranchName)) + return nil + } + // we have pushed the base branch as the new branch, now we need to commit the changes directly to the new branch + oldBranchName = targetBranchName + } + + return &preparedEditorCommitForm[T]{ + form: form, + commonForm: commonForm, + CommitFormOptions: commitFormOptions, + OldBranchName: oldBranchName, + NewBranchName: targetBranchName, + GitCommitter: gitCommitter, + } } // redirectForCommitChoice redirects after committing the edit to a branch -func redirectForCommitChoice(ctx *context.Context, commitChoice, newBranchName, treePath string) { - if commitChoice == frmCommitChoiceNewBranch { +func redirectForCommitChoice[T any](ctx *context.Context, parsed *preparedEditorCommitForm[T], treePath string) { + // when editing a file in a PR, it should return to the origin location + if returnURI := ctx.FormString("return_uri"); returnURI != "" && httplib.IsCurrentGiteaSiteURL(ctx, returnURI) { + ctx.JSONRedirect(returnURI) + return + } + + if parsed.commonForm.CommitChoice == editorCommitChoiceNewBranch { // Redirect to a pull request when possible redirectToPullRequest := false - repo := ctx.Repo.Repository - baseBranch := ctx.Repo.BranchName - headBranch := newBranchName - if repo.UnitEnabled(ctx, unit.TypePullRequests) { - redirectToPullRequest = true - } else if canCreateBasePullRequest(ctx) { + repo, baseBranch, headBranch := ctx.Repo.Repository, parsed.OldBranchName, parsed.NewBranchName + if ctx.Repo.Repository.IsFork && parsed.CommitFormOptions.CanCreateBasePullRequest { redirectToPullRequest = true baseBranch = repo.BaseRepo.DefaultBranch headBranch = repo.Owner.Name + "/" + repo.Name + ":" + headBranch repo = repo.BaseRepo + } else if repo.UnitEnabled(ctx, unit.TypePullRequests) { + redirectToPullRequest = true } - if redirectToPullRequest { - ctx.Redirect(repo.Link() + "/compare/" + util.PathEscapeSegments(baseBranch) + "..." + util.PathEscapeSegments(headBranch)) + ctx.JSONRedirect(repo.Link() + "/compare/" + util.PathEscapeSegments(baseBranch) + "..." + util.PathEscapeSegments(headBranch)) return } } - returnURI := ctx.FormString("return_uri") - - ctx.RedirectToCurrentSite( - returnURI, - ctx.Repo.RepoLink+"/src/branch/"+util.PathEscapeSegments(newBranchName)+"/"+util.PathEscapeSegments(treePath), - ) + // redirect to the newly updated file + redirectTo := util.URLJoin(ctx.Repo.RepoLink, "src/branch", util.PathEscapeSegments(parsed.NewBranchName), util.PathEscapeSegments(treePath)) + ctx.JSONRedirect(redirectTo) } -// getParentTreeFields returns list of parent tree names and corresponding tree paths -// based on given tree path. -func getParentTreeFields(treePath string) (treeNames, treePaths []string) { - if len(treePath) == 0 { - return treeNames, treePaths +func editFileOpenExisting(ctx *context.Context) (prefetch []byte, dataRc io.ReadCloser, fInfo *fileInfo) { + entry, err := ctx.Repo.Commit.GetTreeEntryByPath(ctx.Repo.TreePath) + if err != nil { + HandleGitError(ctx, "GetTreeEntryByPath", err) + return nil, nil, nil } - treeNames = strings.Split(treePath, "/") - treePaths = make([]string, len(treeNames)) - for i := range treeNames { - treePaths[i] = strings.Join(treeNames[:i+1], "/") + // No way to edit a directory online. + if entry.IsDir() { + ctx.NotFound(nil) + return nil, nil, nil } - return treeNames, treePaths -} -func editFileCommon(ctx *context.Context, isNewFile bool) { - ctx.Data["PageIsEdit"] = true - ctx.Data["IsNewFile"] = isNewFile - ctx.Data["BranchLink"] = ctx.Repo.RepoLink + "/src/" + ctx.Repo.RefTypeNameSubURL() - ctx.Data["PreviewableExtensions"] = strings.Join(markup.PreviewableExtensions(), ",") - ctx.Data["LineWrapExtensions"] = strings.Join(setting.Repository.Editor.LineWrapExtensions, ",") - ctx.Data["IsEditingFileOnly"] = ctx.FormString("return_uri") != "" - ctx.Data["ReturnURI"] = ctx.FormString("return_uri") -} - -func editFile(ctx *context.Context, isNewFile bool) { - editFileCommon(ctx, isNewFile) - canCommit := renderCommitRights(ctx) - - treePath := cleanUploadFileName(ctx.Repo.TreePath) - if treePath != ctx.Repo.TreePath { - if isNewFile { - ctx.Redirect(path.Join(ctx.Repo.RepoLink, "_new", util.PathEscapeSegments(ctx.Repo.BranchName), util.PathEscapeSegments(treePath))) + blob := entry.Blob() + buf, dataRc, fInfo, err := getFileReader(ctx, ctx.Repo.Repository.ID, blob) + if err != nil { + if git.IsErrNotExist(err) { + ctx.NotFound(err) } else { - ctx.Redirect(path.Join(ctx.Repo.RepoLink, "_edit", util.PathEscapeSegments(ctx.Repo.BranchName), util.PathEscapeSegments(treePath))) + ctx.ServerError("getFileReader", err) } - return + return nil, nil, nil } - // Check if the filename (and additional path) is specified in the querystring - // (filename is a misnomer, but kept for compatibility with GitHub) - filePath, fileName := path.Split(ctx.Req.URL.Query().Get("filename")) - filePath = strings.Trim(filePath, "/") - treeNames, treePaths := getParentTreeFields(path.Join(ctx.Repo.TreePath, filePath)) - - if !isNewFile { - entry, err := ctx.Repo.Commit.GetTreeEntryByPath(ctx.Repo.TreePath) + if fInfo.isLFSFile { + lfsLock, err := git_model.GetTreePathLock(ctx, ctx.Repo.Repository.ID, ctx.Repo.TreePath) if err != nil { - HandleGitError(ctx, "Repo.Commit.GetTreeEntryByPath", err) - return - } - - // No way to edit a directory online. - if entry.IsDir() { + _ = dataRc.Close() + ctx.ServerError("GetTreePathLock", err) + return nil, nil, nil + } else if lfsLock != nil && lfsLock.OwnerID != ctx.Doer.ID { + _ = dataRc.Close() ctx.NotFound(nil) - return + return nil, nil, nil } + } - blob := entry.Blob() + return buf, dataRc, fInfo +} - buf, dataRc, fInfo, err := getFileReader(ctx, ctx.Repo.Repository.ID, blob) - if err != nil { - if git.IsErrNotExist(err) { - ctx.NotFound(err) - } else { - ctx.ServerError("getFileReader", err) - } - return +func EditFile(ctx *context.Context) { + editorAction := ctx.PathParam("editor_action") + isNewFile := editorAction == "_new" + ctx.Data["IsNewFile"] = isNewFile + + // Check if the filename (and additional path) is specified in the querystring + // (filename is a misnomer, but kept for compatibility with GitHub) + urlQuery := ctx.Req.URL.Query() + queryFilename := urlQuery.Get("filename") + if queryFilename != "" { + newTreePath := path.Join(ctx.Repo.TreePath, queryFilename) + redirectTo := fmt.Sprintf("%s/%s/%s/%s", ctx.Repo.RepoLink, editorAction, util.PathEscapeSegments(ctx.Repo.BranchName), util.PathEscapeSegments(newTreePath)) + urlQuery.Del("filename") + if newQueryParams := urlQuery.Encode(); newQueryParams != "" { + redirectTo += "?" + newQueryParams } + ctx.Redirect(redirectTo) + return + } - defer dataRc.Close() + // on the "New File" page, we should add an empty path field to make end users could input a new name + prepareTreePathFieldsAndPaths(ctx, util.Iif(isNewFile, ctx.Repo.TreePath+"/", ctx.Repo.TreePath)) - if fInfo.isLFSFile { - lfsLock, err := git_model.GetTreePathLock(ctx, ctx.Repo.Repository.ID, ctx.Repo.TreePath) - if err != nil { - ctx.ServerError("GetTreePathLock", err) - return - } - if lfsLock != nil && lfsLock.OwnerID != ctx.Doer.ID { - ctx.NotFound(nil) - return - } + prepareEditorCommitFormOptions(ctx, editorAction) + if ctx.Written() { + return + } + + if !isNewFile { + prefetch, dataRc, fInfo := editFileOpenExisting(ctx) + if ctx.Written() { + return } + defer dataRc.Close() ctx.Data["FileSize"] = fInfo.fileSize @@ -179,740 +298,152 @@ func editFile(ctx *context.Context, isNewFile bool) { ctx.Data["NotEditableReason"] = ctx.Tr("repo.editor.cannot_edit_non_text_files") } else if fInfo.fileSize >= setting.UI.MaxDisplayFileSize { ctx.Data["NotEditableReason"] = ctx.Tr("repo.editor.cannot_edit_too_large_file") - } else { - d, _ := io.ReadAll(dataRc) + } - buf = append(buf, d...) + if ctx.Data["NotEditableReason"] == nil { + buf, err := io.ReadAll(io.MultiReader(bytes.NewReader(prefetch), dataRc)) + if err != nil { + ctx.ServerError("ReadAll", err) + return + } if content, err := charset.ToUTF8(buf, charset.ConvertOpts{KeepBOM: true}); err != nil { - log.Error("ToUTF8: %v", err) ctx.Data["FileContent"] = string(buf) } else { ctx.Data["FileContent"] = content } } - } else { - // Append filename from query, or empty string to allow username the new file. - treeNames = append(treeNames, fileName) } - ctx.Data["TreeNames"] = treeNames - ctx.Data["TreePaths"] = treePaths - ctx.Data["commit_summary"] = "" - ctx.Data["commit_message"] = "" - ctx.Data["commit_choice"] = util.Iif(canCommit, frmCommitChoiceDirect, frmCommitChoiceNewBranch) - ctx.Data["new_branch_name"] = GetUniquePatchBranchName(ctx) - ctx.Data["last_commit"] = ctx.Repo.CommitID - - ctx.Data["EditorconfigJson"] = GetEditorConfig(ctx, treePath) - + ctx.Data["EditorconfigJson"] = getContextRepoEditorConfig(ctx, ctx.Repo.TreePath) ctx.HTML(http.StatusOK, tplEditFile) } -// GetEditorConfig returns a editorconfig JSON string for given treePath or "null" -func GetEditorConfig(ctx *context.Context, treePath string) string { - ec, _, err := ctx.Repo.GetEditorconfig() - if err == nil { - def, err := ec.GetDefinitionForFilename(treePath) - if err == nil { - jsonStr, _ := json.Marshal(def) - return string(jsonStr) - } - } - return "null" -} - -// EditFile render edit file page -func EditFile(ctx *context.Context) { - editFile(ctx, false) -} - -// NewFile render create file page -func NewFile(ctx *context.Context) { - editFile(ctx, true) -} - -func editFilePost(ctx *context.Context, form forms.EditRepoFileForm, isNewFile bool) { - editFileCommon(ctx, isNewFile) - ctx.Data["PageHasPosted"] = true - - canCommit := renderCommitRights(ctx) - treeNames, treePaths := getParentTreeFields(form.TreePath) - branchName := ctx.Repo.BranchName - if form.CommitChoice == frmCommitChoiceNewBranch { - branchName = form.NewBranchName - } - - ctx.Data["TreePath"] = form.TreePath - ctx.Data["TreeNames"] = treeNames - ctx.Data["TreePaths"] = treePaths - ctx.Data["FileContent"] = form.Content - ctx.Data["commit_summary"] = form.CommitSummary - ctx.Data["commit_message"] = form.CommitMessage - ctx.Data["commit_choice"] = form.CommitChoice - ctx.Data["new_branch_name"] = form.NewBranchName - ctx.Data["last_commit"] = ctx.Repo.CommitID - ctx.Data["EditorconfigJson"] = GetEditorConfig(ctx, form.TreePath) - - if ctx.HasError() { - ctx.HTML(http.StatusOK, tplEditFile) - return - } - - // Cannot commit to an existing branch if user doesn't have rights - if branchName == ctx.Repo.BranchName && !canCommit { - ctx.Data["Err_NewBranchName"] = true - ctx.Data["commit_choice"] = frmCommitChoiceNewBranch - ctx.RenderWithErr(ctx.Tr("repo.editor.cannot_commit_to_protected_branch", branchName), tplEditFile, &form) +func EditFilePost(ctx *context.Context) { + editorAction := ctx.PathParam("editor_action") + isNewFile := editorAction == "_new" + parsed := prepareEditorCommitSubmittedForm[*forms.EditRepoFileForm](ctx) + if ctx.Written() { return } - // CommitSummary is optional in the web form, if empty, give it a default message based on add or update - // `message` will be both the summary and message combined - message := strings.TrimSpace(form.CommitSummary) - if len(message) == 0 { - if isNewFile { - message = ctx.Locale.TrString("repo.editor.add", form.TreePath) - } else { - message = ctx.Locale.TrString("repo.editor.update", form.TreePath) - } - } - form.CommitMessage = strings.TrimSpace(form.CommitMessage) - if len(form.CommitMessage) > 0 { - message += "\n\n" + form.CommitMessage - } - - gitCommitter, valid := WebGitOperationGetCommitChosenEmailIdentity(ctx, form.CommitEmail) - if !valid { - ctx.Data["Err_CommitEmail"] = true - ctx.RenderWithErr(ctx.Tr("repo.editor.invalid_commit_email"), tplEditFile, &form) - return - } + defaultCommitMessage := util.Iif(isNewFile, ctx.Locale.TrString("repo.editor.add", parsed.form.TreePath), ctx.Locale.TrString("repo.editor.update", parsed.form.TreePath)) var operation string if isNewFile { operation = "create" - } else if form.Content.Has() { + } else if parsed.form.Content.Has() { // The form content only has data if the file is representable as text, is not too large and not in lfs. operation = "update" - } else if ctx.Repo.TreePath != form.TreePath { + } else if ctx.Repo.TreePath != parsed.form.TreePath { // If it doesn't have data, the only possible operation is a "rename" operation = "rename" } else { // It should never happen, just in case - ctx.Flash.Error(ctx.Tr("error.occurred")) - ctx.HTML(http.StatusOK, tplEditFile) + ctx.JSONError(ctx.Tr("error.occurred")) return } - if _, err := files_service.ChangeRepoFiles(ctx, ctx.Repo.Repository, ctx.Doer, &files_service.ChangeRepoFilesOptions{ - LastCommitID: form.LastCommit, - OldBranch: ctx.Repo.BranchName, - NewBranch: branchName, - Message: message, + _, err := files_service.ChangeRepoFiles(ctx, ctx.Repo.Repository, ctx.Doer, &files_service.ChangeRepoFilesOptions{ + LastCommitID: parsed.form.LastCommit, + OldBranch: parsed.OldBranchName, + NewBranch: parsed.NewBranchName, + Message: parsed.GetCommitMessage(defaultCommitMessage), Files: []*files_service.ChangeRepoFile{ { Operation: operation, FromTreePath: ctx.Repo.TreePath, - TreePath: form.TreePath, - ContentReader: strings.NewReader(strings.ReplaceAll(form.Content.Value(), "\r", "")), + TreePath: parsed.form.TreePath, + ContentReader: strings.NewReader(strings.ReplaceAll(parsed.form.Content.Value(), "\r", "")), }, }, - Signoff: form.Signoff, - Author: gitCommitter, - Committer: gitCommitter, - }); err != nil { - // This is where we handle all the errors thrown by files_service.ChangeRepoFiles - if git.IsErrNotExist(err) { - ctx.RenderWithErr(ctx.Tr("repo.editor.file_editing_no_longer_exists", ctx.Repo.TreePath), tplEditFile, &form) - } else if git_model.IsErrLFSFileLocked(err) { - ctx.Data["Err_TreePath"] = true - ctx.RenderWithErr(ctx.Tr("repo.editor.upload_file_is_locked", err.(git_model.ErrLFSFileLocked).Path, err.(git_model.ErrLFSFileLocked).UserName), tplEditFile, &form) - } else if files_service.IsErrFilenameInvalid(err) { - ctx.Data["Err_TreePath"] = true - ctx.RenderWithErr(ctx.Tr("repo.editor.filename_is_invalid", form.TreePath), tplEditFile, &form) - } else if files_service.IsErrFilePathInvalid(err) { - ctx.Data["Err_TreePath"] = true - if fileErr, ok := err.(files_service.ErrFilePathInvalid); ok { - switch fileErr.Type { - case git.EntryModeSymlink: - ctx.RenderWithErr(ctx.Tr("repo.editor.file_is_a_symlink", fileErr.Path), tplEditFile, &form) - case git.EntryModeTree: - ctx.RenderWithErr(ctx.Tr("repo.editor.filename_is_a_directory", fileErr.Path), tplEditFile, &form) - case git.EntryModeBlob: - ctx.RenderWithErr(ctx.Tr("repo.editor.directory_is_a_file", fileErr.Path), tplEditFile, &form) - default: - ctx.RenderWithErr(ctx.Tr("repo.editor.filename_is_invalid", fileErr.Path), tplEditFile, &form) - } - } else { - ctx.HTTPError(http.StatusInternalServerError, err.Error()) - } - } else if files_service.IsErrRepoFileAlreadyExists(err) { - ctx.Data["Err_TreePath"] = true - ctx.RenderWithErr(ctx.Tr("repo.editor.file_already_exists", form.TreePath), tplEditFile, &form) - } else if git.IsErrBranchNotExist(err) { - // For when a user adds/updates a file to a branch that no longer exists - if branchErr, ok := err.(git.ErrBranchNotExist); ok { - ctx.RenderWithErr(ctx.Tr("repo.editor.branch_does_not_exist", branchErr.Name), tplEditFile, &form) - } else { - ctx.HTTPError(http.StatusInternalServerError, err.Error()) - } - } else if git_model.IsErrBranchAlreadyExists(err) { - // For when a user specifies a new branch that already exists - ctx.Data["Err_NewBranchName"] = true - if branchErr, ok := err.(git_model.ErrBranchAlreadyExists); ok { - ctx.RenderWithErr(ctx.Tr("repo.editor.branch_already_exists", branchErr.BranchName), tplEditFile, &form) - } else { - ctx.HTTPError(http.StatusInternalServerError, err.Error()) - } - } else if files_service.IsErrCommitIDDoesNotMatch(err) { - ctx.RenderWithErr(ctx.Tr("repo.editor.commit_id_not_matching"), tplEditFile, &form) - } else if git.IsErrPushOutOfDate(err) { - ctx.RenderWithErr(ctx.Tr("repo.editor.push_out_of_date"), tplEditFile, &form) - } else if git.IsErrPushRejected(err) { - errPushRej := err.(*git.ErrPushRejected) - if len(errPushRej.Message) == 0 { - ctx.RenderWithErr(ctx.Tr("repo.editor.push_rejected_no_message"), tplEditFile, &form) - } else { - flashError, err := ctx.RenderToHTML(tplAlertDetails, map[string]any{ - "Message": ctx.Tr("repo.editor.push_rejected"), - "Summary": ctx.Tr("repo.editor.push_rejected_summary"), - "Details": utils.SanitizeFlashErrorString(errPushRej.Message), - }) - if err != nil { - ctx.ServerError("editFilePost.HTMLString", err) - return - } - ctx.RenderWithErr(flashError, tplEditFile, &form) - } - } else { - flashError, err := ctx.RenderToHTML(tplAlertDetails, map[string]any{ - "Message": ctx.Tr("repo.editor.fail_to_update_file", form.TreePath), - "Summary": ctx.Tr("repo.editor.fail_to_update_file_summary"), - "Details": utils.SanitizeFlashErrorString(err.Error()), - }) - if err != nil { - ctx.ServerError("editFilePost.HTMLString", err) - return - } - ctx.RenderWithErr(flashError, tplEditFile, &form) - } - } - - redirectForCommitChoice(ctx, form.CommitChoice, branchName, form.TreePath) -} - -// EditFilePost response for editing file -func EditFilePost(ctx *context.Context) { - form := web.GetForm(ctx).(*forms.EditRepoFileForm) - editFilePost(ctx, *form, false) -} - -// NewFilePost response for creating file -func NewFilePost(ctx *context.Context) { - form := web.GetForm(ctx).(*forms.EditRepoFileForm) - editFilePost(ctx, *form, true) -} - -// DiffPreviewPost render preview diff page -func DiffPreviewPost(ctx *context.Context) { - form := web.GetForm(ctx).(*forms.EditPreviewDiffForm) - treePath := cleanUploadFileName(ctx.Repo.TreePath) - if len(treePath) == 0 { - ctx.HTTPError(http.StatusInternalServerError, "file name to diff is invalid") - return - } - - entry, err := ctx.Repo.Commit.GetTreeEntryByPath(treePath) - if err != nil { - ctx.HTTPError(http.StatusInternalServerError, "GetTreeEntryByPath: "+err.Error()) - return - } else if entry.IsDir() { - ctx.HTTPError(http.StatusUnprocessableEntity) - return - } - - diff, err := files_service.GetDiffPreview(ctx, ctx.Repo.Repository, ctx.Repo.BranchName, treePath, form.Content) + Signoff: parsed.form.Signoff, + Author: parsed.GitCommitter, + Committer: parsed.GitCommitter, + }) if err != nil { - ctx.HTTPError(http.StatusInternalServerError, "GetDiffPreview: "+err.Error()) + editorHandleFileOperationError(ctx, parsed.NewBranchName, err) return } - if len(diff.Files) != 0 { - ctx.Data["File"] = diff.Files[0] - } - - ctx.HTML(http.StatusOK, tplEditDiffPreview) + redirectForCommitChoice(ctx, parsed, parsed.form.TreePath) } // DeleteFile render delete file page func DeleteFile(ctx *context.Context) { - ctx.Data["PageIsDelete"] = true - ctx.Data["BranchLink"] = ctx.Repo.RepoLink + "/src/" + ctx.Repo.RefTypeNameSubURL() - treePath := cleanUploadFileName(ctx.Repo.TreePath) - - if treePath != ctx.Repo.TreePath { - ctx.Redirect(path.Join(ctx.Repo.RepoLink, "_delete", util.PathEscapeSegments(ctx.Repo.BranchName), util.PathEscapeSegments(treePath))) + prepareEditorCommitFormOptions(ctx, "_delete") + if ctx.Written() { return } - - ctx.Data["TreePath"] = treePath - canCommit := renderCommitRights(ctx) - - ctx.Data["commit_summary"] = "" - ctx.Data["commit_message"] = "" - ctx.Data["last_commit"] = ctx.Repo.CommitID - if canCommit { - ctx.Data["commit_choice"] = frmCommitChoiceDirect - } else { - ctx.Data["commit_choice"] = frmCommitChoiceNewBranch - } - ctx.Data["new_branch_name"] = GetUniquePatchBranchName(ctx) - + ctx.Data["PageIsDelete"] = true ctx.HTML(http.StatusOK, tplDeleteFile) } // DeleteFilePost response for deleting file func DeleteFilePost(ctx *context.Context) { - form := web.GetForm(ctx).(*forms.DeleteRepoFileForm) - canCommit := renderCommitRights(ctx) - branchName := ctx.Repo.BranchName - if form.CommitChoice == frmCommitChoiceNewBranch { - branchName = form.NewBranchName - } - - ctx.Data["PageIsDelete"] = true - ctx.Data["BranchLink"] = ctx.Repo.RepoLink + "/src/" + ctx.Repo.RefTypeNameSubURL() - ctx.Data["TreePath"] = ctx.Repo.TreePath - ctx.Data["commit_summary"] = form.CommitSummary - ctx.Data["commit_message"] = form.CommitMessage - ctx.Data["commit_choice"] = form.CommitChoice - ctx.Data["new_branch_name"] = form.NewBranchName - ctx.Data["last_commit"] = ctx.Repo.CommitID - - if ctx.HasError() { - ctx.HTML(http.StatusOK, tplDeleteFile) + parsed := prepareEditorCommitSubmittedForm[*forms.DeleteRepoFileForm](ctx) + if ctx.Written() { return } - if branchName == ctx.Repo.BranchName && !canCommit { - ctx.Data["Err_NewBranchName"] = true - ctx.Data["commit_choice"] = frmCommitChoiceNewBranch - ctx.RenderWithErr(ctx.Tr("repo.editor.cannot_commit_to_protected_branch", branchName), tplDeleteFile, &form) - return - } - - message := strings.TrimSpace(form.CommitSummary) - if len(message) == 0 { - message = ctx.Locale.TrString("repo.editor.delete", ctx.Repo.TreePath) - } - form.CommitMessage = strings.TrimSpace(form.CommitMessage) - if len(form.CommitMessage) > 0 { - message += "\n\n" + form.CommitMessage - } - - gitCommitter, valid := WebGitOperationGetCommitChosenEmailIdentity(ctx, form.CommitEmail) - if !valid { - ctx.Data["Err_CommitEmail"] = true - ctx.RenderWithErr(ctx.Tr("repo.editor.invalid_commit_email"), tplDeleteFile, &form) - return - } - - if _, err := files_service.ChangeRepoFiles(ctx, ctx.Repo.Repository, ctx.Doer, &files_service.ChangeRepoFilesOptions{ - LastCommitID: form.LastCommit, - OldBranch: ctx.Repo.BranchName, - NewBranch: branchName, + treePath := ctx.Repo.TreePath + _, err := files_service.ChangeRepoFiles(ctx, ctx.Repo.Repository, ctx.Doer, &files_service.ChangeRepoFilesOptions{ + LastCommitID: parsed.form.LastCommit, + OldBranch: parsed.OldBranchName, + NewBranch: parsed.NewBranchName, Files: []*files_service.ChangeRepoFile{ { Operation: "delete", - TreePath: ctx.Repo.TreePath, + TreePath: treePath, }, }, - Message: message, - Signoff: form.Signoff, - Author: gitCommitter, - Committer: gitCommitter, - }); err != nil { - // This is where we handle all the errors thrown by repofiles.DeleteRepoFile - if git.IsErrNotExist(err) || files_service.IsErrRepoFileDoesNotExist(err) { - ctx.RenderWithErr(ctx.Tr("repo.editor.file_deleting_no_longer_exists", ctx.Repo.TreePath), tplDeleteFile, &form) - } else if files_service.IsErrFilenameInvalid(err) { - ctx.Data["Err_TreePath"] = true - ctx.RenderWithErr(ctx.Tr("repo.editor.filename_is_invalid", ctx.Repo.TreePath), tplDeleteFile, &form) - } else if files_service.IsErrFilePathInvalid(err) { - ctx.Data["Err_TreePath"] = true - if fileErr, ok := err.(files_service.ErrFilePathInvalid); ok { - switch fileErr.Type { - case git.EntryModeSymlink: - ctx.RenderWithErr(ctx.Tr("repo.editor.file_is_a_symlink", fileErr.Path), tplDeleteFile, &form) - case git.EntryModeTree: - ctx.RenderWithErr(ctx.Tr("repo.editor.filename_is_a_directory", fileErr.Path), tplDeleteFile, &form) - case git.EntryModeBlob: - ctx.RenderWithErr(ctx.Tr("repo.editor.directory_is_a_file", fileErr.Path), tplDeleteFile, &form) - default: - ctx.ServerError("DeleteRepoFile", err) - } - } else { - ctx.ServerError("DeleteRepoFile", err) - } - } else if git.IsErrBranchNotExist(err) { - // For when a user deletes a file to a branch that no longer exists - if branchErr, ok := err.(git.ErrBranchNotExist); ok { - ctx.RenderWithErr(ctx.Tr("repo.editor.branch_does_not_exist", branchErr.Name), tplDeleteFile, &form) - } else { - ctx.HTTPError(http.StatusInternalServerError, err.Error()) - } - } else if git_model.IsErrBranchAlreadyExists(err) { - // For when a user specifies a new branch that already exists - if branchErr, ok := err.(git_model.ErrBranchAlreadyExists); ok { - ctx.RenderWithErr(ctx.Tr("repo.editor.branch_already_exists", branchErr.BranchName), tplDeleteFile, &form) - } else { - ctx.HTTPError(http.StatusInternalServerError, err.Error()) - } - } else if files_service.IsErrCommitIDDoesNotMatch(err) || git.IsErrPushOutOfDate(err) { - ctx.RenderWithErr(ctx.Tr("repo.editor.file_changed_while_deleting", ctx.Repo.RepoLink+"/compare/"+util.PathEscapeSegments(form.LastCommit)+"..."+util.PathEscapeSegments(ctx.Repo.CommitID)), tplDeleteFile, &form) - } else if git.IsErrPushRejected(err) { - errPushRej := err.(*git.ErrPushRejected) - if len(errPushRej.Message) == 0 { - ctx.RenderWithErr(ctx.Tr("repo.editor.push_rejected_no_message"), tplDeleteFile, &form) - } else { - flashError, err := ctx.RenderToHTML(tplAlertDetails, map[string]any{ - "Message": ctx.Tr("repo.editor.push_rejected"), - "Summary": ctx.Tr("repo.editor.push_rejected_summary"), - "Details": utils.SanitizeFlashErrorString(errPushRej.Message), - }) - if err != nil { - ctx.ServerError("DeleteFilePost.HTMLString", err) - return - } - ctx.RenderWithErr(flashError, tplDeleteFile, &form) - } - } else { - ctx.ServerError("DeleteRepoFile", err) - } + Message: parsed.GetCommitMessage(ctx.Locale.TrString("repo.editor.delete", treePath)), + Signoff: parsed.form.Signoff, + Author: parsed.GitCommitter, + Committer: parsed.GitCommitter, + }) + if err != nil { + editorHandleFileOperationError(ctx, parsed.NewBranchName, err) return } - ctx.Flash.Success(ctx.Tr("repo.editor.file_delete_success", ctx.Repo.TreePath)) - treePath := path.Dir(ctx.Repo.TreePath) - if treePath == "." { - treePath = "" // the file deleted was in the root, so we return the user to the root directory - } - if len(treePath) > 0 { - // Need to get the latest commit since it changed - commit, err := ctx.Repo.GitRepo.GetBranchCommit(ctx.Repo.BranchName) - if err == nil && commit != nil { - // We have the comment, now find what directory we can return the user to - // (must have entries) - treePath = GetClosestParentWithFiles(treePath, commit) - } else { - treePath = "" // otherwise return them to the root of the repo - } - } - - redirectForCommitChoice(ctx, form.CommitChoice, branchName, treePath) + ctx.Flash.Success(ctx.Tr("repo.editor.file_delete_success", treePath)) + redirectTreePath := getClosestParentWithFiles(ctx.Repo.GitRepo, parsed.NewBranchName, treePath) + redirectForCommitChoice(ctx, parsed, redirectTreePath) } -// UploadFile render upload file page func UploadFile(ctx *context.Context) { ctx.Data["PageIsUpload"] = true - upload.AddUploadContext(ctx, "repo") - canCommit := renderCommitRights(ctx) - treePath := cleanUploadFileName(ctx.Repo.TreePath) - if treePath != ctx.Repo.TreePath { - ctx.Redirect(path.Join(ctx.Repo.RepoLink, "_upload", util.PathEscapeSegments(ctx.Repo.BranchName), util.PathEscapeSegments(treePath))) + prepareTreePathFieldsAndPaths(ctx, ctx.Repo.TreePath) + opts := prepareEditorCommitFormOptions(ctx, "_upload") + if ctx.Written() { return } - ctx.Repo.TreePath = treePath - - treeNames, treePaths := getParentTreeFields(ctx.Repo.TreePath) - if len(treeNames) == 0 { - // We must at least have one element for user to input. - treeNames = []string{""} - } - - ctx.Data["TreeNames"] = treeNames - ctx.Data["TreePaths"] = treePaths - ctx.Data["BranchLink"] = ctx.Repo.RepoLink + "/src/" + ctx.Repo.RefTypeNameSubURL() - ctx.Data["commit_summary"] = "" - ctx.Data["commit_message"] = "" - if canCommit { - ctx.Data["commit_choice"] = frmCommitChoiceDirect - } else { - ctx.Data["commit_choice"] = frmCommitChoiceNewBranch - } - ctx.Data["new_branch_name"] = GetUniquePatchBranchName(ctx) + upload.AddUploadContextForRepo(ctx, opts.TargetRepo) ctx.HTML(http.StatusOK, tplUploadFile) } -// UploadFilePost response for uploading file func UploadFilePost(ctx *context.Context) { - form := web.GetForm(ctx).(*forms.UploadRepoFileForm) - ctx.Data["PageIsUpload"] = true - upload.AddUploadContext(ctx, "repo") - canCommit := renderCommitRights(ctx) - - oldBranchName := ctx.Repo.BranchName - branchName := oldBranchName - - if form.CommitChoice == frmCommitChoiceNewBranch { - branchName = form.NewBranchName - } - - form.TreePath = cleanUploadFileName(form.TreePath) - - treeNames, treePaths := getParentTreeFields(form.TreePath) - if len(treeNames) == 0 { - // We must at least have one element for user to input. - treeNames = []string{""} - } - - ctx.Data["TreePath"] = form.TreePath - ctx.Data["TreeNames"] = treeNames - ctx.Data["TreePaths"] = treePaths - ctx.Data["BranchLink"] = ctx.Repo.RepoLink + "/src/branch/" + util.PathEscapeSegments(branchName) - ctx.Data["commit_summary"] = form.CommitSummary - ctx.Data["commit_message"] = form.CommitMessage - ctx.Data["commit_choice"] = form.CommitChoice - ctx.Data["new_branch_name"] = branchName - - if ctx.HasError() { - ctx.HTML(http.StatusOK, tplUploadFile) - return - } - - if oldBranchName != branchName { - if exist, err := git_model.IsBranchExist(ctx, ctx.Repo.Repository.ID, branchName); err == nil && exist { - ctx.Data["Err_NewBranchName"] = true - ctx.RenderWithErr(ctx.Tr("repo.editor.branch_already_exists", branchName), tplUploadFile, &form) - return - } - } else if !canCommit { - ctx.Data["Err_NewBranchName"] = true - ctx.Data["commit_choice"] = frmCommitChoiceNewBranch - ctx.RenderWithErr(ctx.Tr("repo.editor.cannot_commit_to_protected_branch", branchName), tplUploadFile, &form) - return - } - - if !ctx.Repo.Repository.IsEmpty { - var newTreePath string - for _, part := range treeNames { - newTreePath = path.Join(newTreePath, part) - entry, err := ctx.Repo.Commit.GetTreeEntryByPath(newTreePath) - if err != nil { - if git.IsErrNotExist(err) { - break // Means there is no item with that name, so we're good - } - ctx.ServerError("Repo.Commit.GetTreeEntryByPath", err) - return - } - - // User can only upload files to a directory, the directory name shouldn't be an existing file. - if !entry.IsDir() { - ctx.Data["Err_TreePath"] = true - ctx.RenderWithErr(ctx.Tr("repo.editor.directory_is_a_file", part), tplUploadFile, &form) - return - } - } - } - - message := strings.TrimSpace(form.CommitSummary) - if len(message) == 0 { - dir := form.TreePath - if dir == "" { - dir = "/" - } - message = ctx.Locale.TrString("repo.editor.upload_files_to_dir", dir) - } - - form.CommitMessage = strings.TrimSpace(form.CommitMessage) - if len(form.CommitMessage) > 0 { - message += "\n\n" + form.CommitMessage - } - - gitCommitter, valid := WebGitOperationGetCommitChosenEmailIdentity(ctx, form.CommitEmail) - if !valid { - ctx.Data["Err_CommitEmail"] = true - ctx.RenderWithErr(ctx.Tr("repo.editor.invalid_commit_email"), tplUploadFile, &form) - return - } - - if err := files_service.UploadRepoFiles(ctx, ctx.Repo.Repository, ctx.Doer, &files_service.UploadRepoFileOptions{ - LastCommitID: ctx.Repo.CommitID, - OldBranch: oldBranchName, - NewBranch: branchName, - TreePath: form.TreePath, - Message: message, - Files: form.Files, - Signoff: form.Signoff, - Author: gitCommitter, - Committer: gitCommitter, - }); err != nil { - if git_model.IsErrLFSFileLocked(err) { - ctx.Data["Err_TreePath"] = true - ctx.RenderWithErr(ctx.Tr("repo.editor.upload_file_is_locked", err.(git_model.ErrLFSFileLocked).Path, err.(git_model.ErrLFSFileLocked).UserName), tplUploadFile, &form) - } else if files_service.IsErrFilenameInvalid(err) { - ctx.Data["Err_TreePath"] = true - ctx.RenderWithErr(ctx.Tr("repo.editor.filename_is_invalid", form.TreePath), tplUploadFile, &form) - } else if files_service.IsErrFilePathInvalid(err) { - ctx.Data["Err_TreePath"] = true - fileErr := err.(files_service.ErrFilePathInvalid) - switch fileErr.Type { - case git.EntryModeSymlink: - ctx.RenderWithErr(ctx.Tr("repo.editor.file_is_a_symlink", fileErr.Path), tplUploadFile, &form) - case git.EntryModeTree: - ctx.RenderWithErr(ctx.Tr("repo.editor.filename_is_a_directory", fileErr.Path), tplUploadFile, &form) - case git.EntryModeBlob: - ctx.RenderWithErr(ctx.Tr("repo.editor.directory_is_a_file", fileErr.Path), tplUploadFile, &form) - default: - ctx.HTTPError(http.StatusInternalServerError, err.Error()) - } - } else if files_service.IsErrRepoFileAlreadyExists(err) { - ctx.Data["Err_TreePath"] = true - ctx.RenderWithErr(ctx.Tr("repo.editor.file_already_exists", form.TreePath), tplUploadFile, &form) - } else if git.IsErrBranchNotExist(err) { - branchErr := err.(git.ErrBranchNotExist) - ctx.RenderWithErr(ctx.Tr("repo.editor.branch_does_not_exist", branchErr.Name), tplUploadFile, &form) - } else if git_model.IsErrBranchAlreadyExists(err) { - // For when a user specifies a new branch that already exists - ctx.Data["Err_NewBranchName"] = true - branchErr := err.(git_model.ErrBranchAlreadyExists) - ctx.RenderWithErr(ctx.Tr("repo.editor.branch_already_exists", branchErr.BranchName), tplUploadFile, &form) - } else if git.IsErrPushOutOfDate(err) { - ctx.RenderWithErr(ctx.Tr("repo.editor.file_changed_while_editing", ctx.Repo.RepoLink+"/compare/"+util.PathEscapeSegments(ctx.Repo.CommitID)+"..."+util.PathEscapeSegments(form.NewBranchName)), tplUploadFile, &form) - } else if git.IsErrPushRejected(err) { - errPushRej := err.(*git.ErrPushRejected) - if len(errPushRej.Message) == 0 { - ctx.RenderWithErr(ctx.Tr("repo.editor.push_rejected_no_message"), tplUploadFile, &form) - } else { - flashError, err := ctx.RenderToHTML(tplAlertDetails, map[string]any{ - "Message": ctx.Tr("repo.editor.push_rejected"), - "Summary": ctx.Tr("repo.editor.push_rejected_summary"), - "Details": utils.SanitizeFlashErrorString(errPushRej.Message), - }) - if err != nil { - ctx.ServerError("UploadFilePost.HTMLString", err) - return - } - ctx.RenderWithErr(flashError, tplUploadFile, &form) - } - } else { - // os.ErrNotExist - upload file missing in the intervening time?! - log.Error("Error during upload to repo: %-v to filepath: %s on %s from %s: %v", ctx.Repo.Repository, form.TreePath, oldBranchName, form.NewBranchName, err) - ctx.RenderWithErr(ctx.Tr("repo.editor.unable_to_upload_files", form.TreePath, err), tplUploadFile, &form) - } - return - } - - if ctx.Repo.Repository.IsEmpty { - if isEmpty, err := ctx.Repo.GitRepo.IsEmpty(); err == nil && !isEmpty { - _ = repo_model.UpdateRepositoryColsWithAutoTime(ctx, &repo_model.Repository{ID: ctx.Repo.Repository.ID, IsEmpty: false}, "is_empty") - } - } - - redirectForCommitChoice(ctx, form.CommitChoice, branchName, form.TreePath) -} - -func cleanUploadFileName(name string) string { - // Rebase the filename - name = util.PathJoinRel(name) - // Git disallows any filenames to have a .git directory in them. - for part := range strings.SplitSeq(name, "/") { - if strings.ToLower(part) == ".git" { - return "" - } - } - return name -} - -// UploadFileToServer upload file to server file dir not git -func UploadFileToServer(ctx *context.Context) { - file, header, err := ctx.Req.FormFile("file") - if err != nil { - ctx.HTTPError(http.StatusInternalServerError, fmt.Sprintf("FormFile: %v", err)) - return - } - defer file.Close() - - buf := make([]byte, 1024) - n, _ := util.ReadAtMost(file, buf) - if n > 0 { - buf = buf[:n] - } - - err = upload.Verify(buf, header.Filename, setting.Repository.Upload.AllowedTypes) - if err != nil { - ctx.HTTPError(http.StatusBadRequest, err.Error()) - return - } - - name := cleanUploadFileName(header.Filename) - if len(name) == 0 { - ctx.HTTPError(http.StatusInternalServerError, "Upload file name is invalid") + parsed := prepareEditorCommitSubmittedForm[*forms.UploadRepoFileForm](ctx) + if ctx.Written() { return } - upload, err := repo_model.NewUpload(ctx, name, buf, file) - if err != nil { - ctx.HTTPError(http.StatusInternalServerError, fmt.Sprintf("NewUpload: %v", err)) - return - } - - log.Trace("New file uploaded: %s", upload.UUID) - ctx.JSON(http.StatusOK, map[string]string{ - "uuid": upload.UUID, + defaultCommitMessage := ctx.Locale.TrString("repo.editor.upload_files_to_dir", util.IfZero(parsed.form.TreePath, "/")) + err := files_service.UploadRepoFiles(ctx, ctx.Repo.Repository, ctx.Doer, &files_service.UploadRepoFileOptions{ + LastCommitID: parsed.form.LastCommit, + OldBranch: parsed.OldBranchName, + NewBranch: parsed.NewBranchName, + TreePath: parsed.form.TreePath, + Message: parsed.GetCommitMessage(defaultCommitMessage), + Files: parsed.form.Files, + Signoff: parsed.form.Signoff, + Author: parsed.GitCommitter, + Committer: parsed.GitCommitter, }) -} - -// RemoveUploadFileFromServer remove file from server file dir -func RemoveUploadFileFromServer(ctx *context.Context) { - form := web.GetForm(ctx).(*forms.RemoveUploadFileForm) - if len(form.File) == 0 { - ctx.Status(http.StatusNoContent) - return - } - - if err := repo_model.DeleteUploadByUUID(ctx, form.File); err != nil { - ctx.HTTPError(http.StatusInternalServerError, fmt.Sprintf("DeleteUploadByUUID: %v", err)) + if err != nil { + editorHandleFileOperationError(ctx, parsed.NewBranchName, err) return } - - log.Trace("Upload file removed: %s", form.File) - ctx.Status(http.StatusNoContent) -} - -// GetUniquePatchBranchName Gets a unique branch name for a new patch branch -// It will be in the form of <username>-patch-<num> where <num> is the first branch of this format -// that doesn't already exist. If we exceed 1000 tries or an error is thrown, we just return "" so the user has to -// type in the branch name themselves (will be an empty field) -func GetUniquePatchBranchName(ctx *context.Context) string { - prefix := ctx.Doer.LowerName + "-patch-" - for i := 1; i <= 1000; i++ { - branchName := fmt.Sprintf("%s%d", prefix, i) - if exist, err := git_model.IsBranchExist(ctx, ctx.Repo.Repository.ID, branchName); err != nil { - log.Error("GetUniquePatchBranchName: %v", err) - return "" - } else if !exist { - return branchName - } - } - return "" -} - -// GetClosestParentWithFiles Recursively gets the path of parent in a tree that has files (used when file in a tree is -// deleted). Returns "" for the root if no parents other than the root have files. If the given treePath isn't a -// SubTree or it has no entries, we go up one dir and see if we can return the user to that listing. -func GetClosestParentWithFiles(treePath string, commit *git.Commit) string { - if len(treePath) == 0 || treePath == "." { - return "" - } - // see if the tree has entries - if tree, err := commit.SubTree(treePath); err != nil { - // failed to get tree, going up a dir - return GetClosestParentWithFiles(path.Dir(treePath), commit) - } else if entries, err := tree.ListEntries(); err != nil || len(entries) == 0 { - // no files in this dir, going up a dir - return GetClosestParentWithFiles(path.Dir(treePath), commit) - } - return treePath + redirectForCommitChoice(ctx, parsed, parsed.form.TreePath) } diff --git a/routers/web/repo/editor_apply_patch.go b/routers/web/repo/editor_apply_patch.go new file mode 100644 index 0000000000..bd2811cc5f --- /dev/null +++ b/routers/web/repo/editor_apply_patch.go @@ -0,0 +1,51 @@ +// Copyright 2021 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package repo + +import ( + "net/http" + "strings" + + "code.gitea.io/gitea/modules/util" + "code.gitea.io/gitea/services/context" + "code.gitea.io/gitea/services/forms" + "code.gitea.io/gitea/services/repository/files" +) + +func NewDiffPatch(ctx *context.Context) { + prepareEditorCommitFormOptions(ctx, "_diffpatch") + if ctx.Written() { + return + } + + ctx.Data["PageIsPatch"] = true + ctx.HTML(http.StatusOK, tplPatchFile) +} + +// NewDiffPatchPost response for sending patch page +func NewDiffPatchPost(ctx *context.Context) { + parsed := prepareEditorCommitSubmittedForm[*forms.EditRepoFileForm](ctx) + if ctx.Written() { + return + } + + defaultCommitMessage := ctx.Locale.TrString("repo.editor.patch") + _, err := files.ApplyDiffPatch(ctx, ctx.Repo.Repository, ctx.Doer, &files.ApplyDiffPatchOptions{ + LastCommitID: parsed.form.LastCommit, + OldBranch: parsed.OldBranchName, + NewBranch: parsed.NewBranchName, + Message: parsed.GetCommitMessage(defaultCommitMessage), + Content: strings.ReplaceAll(parsed.form.Content.Value(), "\r\n", "\n"), + Author: parsed.GitCommitter, + Committer: parsed.GitCommitter, + }) + if err != nil { + err = util.ErrorWrapLocale(err, "repo.editor.fail_to_apply_patch") + } + if err != nil { + editorHandleFileOperationError(ctx, parsed.NewBranchName, err) + return + } + redirectForCommitChoice(ctx, parsed, parsed.form.TreePath) +} diff --git a/routers/web/repo/editor_cherry_pick.go b/routers/web/repo/editor_cherry_pick.go new file mode 100644 index 0000000000..10c2741b1c --- /dev/null +++ b/routers/web/repo/editor_cherry_pick.go @@ -0,0 +1,86 @@ +// Copyright 2021 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package repo + +import ( + "bytes" + "net/http" + "strings" + + "code.gitea.io/gitea/modules/git" + "code.gitea.io/gitea/modules/util" + "code.gitea.io/gitea/services/context" + "code.gitea.io/gitea/services/forms" + "code.gitea.io/gitea/services/repository/files" +) + +func CherryPick(ctx *context.Context) { + prepareEditorCommitFormOptions(ctx, "_cherrypick") + if ctx.Written() { + return + } + + fromCommitID := ctx.PathParam("sha") + ctx.Data["FromCommitID"] = fromCommitID + cherryPickCommit, err := ctx.Repo.GitRepo.GetCommit(fromCommitID) + if err != nil { + HandleGitError(ctx, "GetCommit", err) + return + } + + if ctx.FormString("cherry-pick-type") == "revert" { + ctx.Data["CherryPickType"] = "revert" + ctx.Data["commit_summary"] = "revert " + ctx.PathParam("sha") + ctx.Data["commit_message"] = "revert " + cherryPickCommit.Message() + } else { + ctx.Data["CherryPickType"] = "cherry-pick" + splits := strings.SplitN(cherryPickCommit.Message(), "\n", 2) + ctx.Data["commit_summary"] = splits[0] + ctx.Data["commit_message"] = splits[1] + } + + ctx.HTML(http.StatusOK, tplCherryPick) +} + +func CherryPickPost(ctx *context.Context) { + fromCommitID := ctx.PathParam("sha") + parsed := prepareEditorCommitSubmittedForm[*forms.CherryPickForm](ctx) + if ctx.Written() { + return + } + + defaultCommitMessage := util.Iif(parsed.form.Revert, ctx.Locale.TrString("repo.commit.revert-header", fromCommitID), ctx.Locale.TrString("repo.commit.cherry-pick-header", fromCommitID)) + opts := &files.ApplyDiffPatchOptions{ + LastCommitID: parsed.form.LastCommit, + OldBranch: parsed.OldBranchName, + NewBranch: parsed.NewBranchName, + Message: parsed.GetCommitMessage(defaultCommitMessage), + Author: parsed.GitCommitter, + Committer: parsed.GitCommitter, + } + + // First try the simple plain read-tree -m approach + opts.Content = fromCommitID + if _, err := files.CherryPick(ctx, ctx.Repo.Repository, ctx.Doer, parsed.form.Revert, opts); err != nil { + // Drop through to the "apply" method + buf := &bytes.Buffer{} + if parsed.form.Revert { + err = git.GetReverseRawDiff(ctx, ctx.Repo.Repository.RepoPath(), fromCommitID, buf) + } else { + err = git.GetRawDiff(ctx.Repo.GitRepo, fromCommitID, "patch", buf) + } + if err == nil { + opts.Content = buf.String() + _, err = files.ApplyDiffPatch(ctx, ctx.Repo.Repository, ctx.Doer, opts) + if err != nil { + err = util.ErrorWrapLocale(err, "repo.editor.fail_to_apply_patch") + } + } + if err != nil { + editorHandleFileOperationError(ctx, parsed.NewBranchName, err) + return + } + } + redirectForCommitChoice(ctx, parsed, parsed.form.TreePath) +} diff --git a/routers/web/repo/editor_error.go b/routers/web/repo/editor_error.go new file mode 100644 index 0000000000..245226a039 --- /dev/null +++ b/routers/web/repo/editor_error.go @@ -0,0 +1,82 @@ +// Copyright 2025 Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package repo + +import ( + "errors" + + git_model "code.gitea.io/gitea/models/git" + "code.gitea.io/gitea/modules/git" + "code.gitea.io/gitea/modules/log" + "code.gitea.io/gitea/modules/setting" + "code.gitea.io/gitea/modules/util" + "code.gitea.io/gitea/routers/utils" + context_service "code.gitea.io/gitea/services/context" + files_service "code.gitea.io/gitea/services/repository/files" +) + +func errorAs[T error](v error) (e T, ok bool) { + if errors.As(v, &e) { + return e, true + } + return e, false +} + +func editorHandleFileOperationErrorRender(ctx *context_service.Context, message, summary, details string) { + flashError, err := ctx.RenderToHTML(tplAlertDetails, map[string]any{ + "Message": message, + "Summary": summary, + "Details": utils.SanitizeFlashErrorString(details), + }) + if err == nil { + ctx.JSONError(flashError) + } else { + log.Error("RenderToHTML: %v", err) + ctx.JSONError(message + "\n" + summary + "\n" + utils.SanitizeFlashErrorString(details)) + } +} + +func editorHandleFileOperationError(ctx *context_service.Context, targetBranchName string, err error) { + if errAs := util.ErrorAsLocale(err); errAs != nil { + ctx.JSONError(ctx.Tr(errAs.TrKey, errAs.TrArgs...)) + } else if errAs, ok := errorAs[git.ErrNotExist](err); ok { + ctx.JSONError(ctx.Tr("repo.editor.file_modifying_no_longer_exists", errAs.RelPath)) + } else if errAs, ok := errorAs[git_model.ErrLFSFileLocked](err); ok { + ctx.JSONError(ctx.Tr("repo.editor.upload_file_is_locked", errAs.Path, errAs.UserName)) + } else if errAs, ok := errorAs[files_service.ErrFilenameInvalid](err); ok { + ctx.JSONError(ctx.Tr("repo.editor.filename_is_invalid", errAs.Path)) + } else if errAs, ok := errorAs[files_service.ErrFilePathInvalid](err); ok { + switch errAs.Type { + case git.EntryModeSymlink: + ctx.JSONError(ctx.Tr("repo.editor.file_is_a_symlink", errAs.Path)) + case git.EntryModeTree: + ctx.JSONError(ctx.Tr("repo.editor.filename_is_a_directory", errAs.Path)) + case git.EntryModeBlob: + ctx.JSONError(ctx.Tr("repo.editor.directory_is_a_file", errAs.Path)) + default: + ctx.JSONError(ctx.Tr("repo.editor.filename_is_invalid", errAs.Path)) + } + } else if errAs, ok := errorAs[files_service.ErrRepoFileAlreadyExists](err); ok { + ctx.JSONError(ctx.Tr("repo.editor.file_already_exists", errAs.Path)) + } else if errAs, ok := errorAs[git.ErrBranchNotExist](err); ok { + ctx.JSONError(ctx.Tr("repo.editor.branch_does_not_exist", errAs.Name)) + } else if errAs, ok := errorAs[git_model.ErrBranchAlreadyExists](err); ok { + ctx.JSONError(ctx.Tr("repo.editor.branch_already_exists", errAs.BranchName)) + } else if files_service.IsErrCommitIDDoesNotMatch(err) { + ctx.JSONError(ctx.Tr("repo.editor.commit_id_not_matching")) + } else if files_service.IsErrCommitIDDoesNotMatch(err) || git.IsErrPushOutOfDate(err) { + ctx.JSONError(ctx.Tr("repo.editor.file_changed_while_editing", ctx.Repo.RepoLink+"/compare/"+util.PathEscapeSegments(ctx.Repo.CommitID)+"..."+util.PathEscapeSegments(targetBranchName))) + } else if errAs, ok := errorAs[*git.ErrPushRejected](err); ok { + if errAs.Message == "" { + ctx.JSONError(ctx.Tr("repo.editor.push_rejected_no_message")) + } else { + editorHandleFileOperationErrorRender(ctx, ctx.Locale.TrString("repo.editor.push_rejected"), ctx.Locale.TrString("repo.editor.push_rejected_summary"), errAs.Message) + } + } else if errors.Is(err, util.ErrNotExist) { + ctx.JSONError(ctx.Tr("error.not_found")) + } else { + setting.PanicInDevOrTesting("unclear err %T: %v", err, err) + editorHandleFileOperationErrorRender(ctx, ctx.Locale.TrString("repo.editor.failed_to_commit"), ctx.Locale.TrString("repo.editor.failed_to_commit_summary"), err.Error()) + } +} diff --git a/routers/web/repo/editor_fork.go b/routers/web/repo/editor_fork.go new file mode 100644 index 0000000000..b78a634c00 --- /dev/null +++ b/routers/web/repo/editor_fork.go @@ -0,0 +1,31 @@ +// Copyright 2025 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package repo + +import ( + "net/http" + + "code.gitea.io/gitea/modules/templates" + "code.gitea.io/gitea/services/context" + repo_service "code.gitea.io/gitea/services/repository" +) + +const tplEditorFork templates.TplName = "repo/editor/fork" + +func ForkToEdit(ctx *context.Context) { + ctx.HTML(http.StatusOK, tplEditorFork) +} + +func ForkToEditPost(ctx *context.Context) { + ForkRepoTo(ctx, ctx.Doer, repo_service.ForkRepoOptions{ + BaseRepo: ctx.Repo.Repository, + Name: getUniqueRepositoryName(ctx, ctx.Doer.ID, ctx.Repo.Repository.Name), + Description: ctx.Repo.Repository.Description, + SingleBranch: ctx.Repo.Repository.DefaultBranch, // maybe we only need the default branch in the fork? + }) + if ctx.Written() { + return + } + ctx.JSONRedirect("") // reload the page, the new fork should be editable now +} diff --git a/routers/web/repo/editor_preview.go b/routers/web/repo/editor_preview.go new file mode 100644 index 0000000000..14be5b72b6 --- /dev/null +++ b/routers/web/repo/editor_preview.go @@ -0,0 +1,41 @@ +// Copyright 2025 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package repo + +import ( + "net/http" + + "code.gitea.io/gitea/services/context" + files_service "code.gitea.io/gitea/services/repository/files" +) + +func DiffPreviewPost(ctx *context.Context) { + content := ctx.FormString("content") + treePath := files_service.CleanGitTreePath(ctx.Repo.TreePath) + if treePath == "" { + ctx.HTTPError(http.StatusBadRequest, "file name to diff is invalid") + return + } + + entry, err := ctx.Repo.Commit.GetTreeEntryByPath(treePath) + if err != nil { + ctx.ServerError("GetTreeEntryByPath", err) + return + } else if entry.IsDir() { + ctx.HTTPError(http.StatusUnprocessableEntity) + return + } + + diff, err := files_service.GetDiffPreview(ctx, ctx.Repo.Repository, ctx.Repo.BranchName, treePath, content) + if err != nil { + ctx.ServerError("GetDiffPreview", err) + return + } + + if len(diff.Files) != 0 { + ctx.Data["File"] = diff.Files[0] + } + + ctx.HTML(http.StatusOK, tplEditDiffPreview) +} diff --git a/routers/web/repo/editor_test.go b/routers/web/repo/editor_test.go index 89bf8f309c..6e2c1d6219 100644 --- a/routers/web/repo/editor_test.go +++ b/routers/web/repo/editor_test.go @@ -6,76 +6,27 @@ package repo import ( "testing" + repo_model "code.gitea.io/gitea/models/repo" "code.gitea.io/gitea/models/unittest" "code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/gitrepo" - "code.gitea.io/gitea/services/contexttest" "github.com/stretchr/testify/assert" ) -func TestCleanUploadName(t *testing.T) { +func TestEditorUtils(t *testing.T) { unittest.PrepareTestEnv(t) - - kases := map[string]string{ - ".git/refs/master": "", - "/root/abc": "root/abc", - "./../../abc": "abc", - "a/../.git": "", - "a/../../../abc": "abc", - "../../../acd": "acd", - "../../.git/abc": "", - "..\\..\\.git/abc": "..\\..\\.git/abc", - "..\\../.git/abc": "", - "..\\../.git": "", - "abc/../def": "def", - ".drone.yml": ".drone.yml", - ".abc/def/.drone.yml": ".abc/def/.drone.yml", - "..drone.yml.": "..drone.yml.", - "..a.dotty...name...": "..a.dotty...name...", - "..a.dotty../.folder../.name...": "..a.dotty../.folder../.name...", - } - for k, v := range kases { - assert.Equal(t, cleanUploadFileName(k), v) - } -} - -func TestGetUniquePatchBranchName(t *testing.T) { - unittest.PrepareTestEnv(t) - ctx, _ := contexttest.MockContext(t, "user2/repo1") - ctx.SetPathParam("id", "1") - contexttest.LoadRepo(t, ctx, 1) - contexttest.LoadRepoCommit(t, ctx) - contexttest.LoadUser(t, ctx, 2) - contexttest.LoadGitRepo(t, ctx) - defer ctx.Repo.GitRepo.Close() - - expectedBranchName := "user2-patch-1" - branchName := GetUniquePatchBranchName(ctx) - assert.Equal(t, expectedBranchName, branchName) -} - -func TestGetClosestParentWithFiles(t *testing.T) { - unittest.PrepareTestEnv(t) - ctx, _ := contexttest.MockContext(t, "user2/repo1") - ctx.SetPathParam("id", "1") - contexttest.LoadRepo(t, ctx, 1) - contexttest.LoadRepoCommit(t, ctx) - contexttest.LoadUser(t, ctx, 2) - contexttest.LoadGitRepo(t, ctx) - defer ctx.Repo.GitRepo.Close() - - repo := ctx.Repo.Repository - branch := repo.DefaultBranch - gitRepo, _ := gitrepo.OpenRepository(git.DefaultContext, repo) - defer gitRepo.Close() - commit, _ := gitRepo.GetBranchCommit(branch) - var expectedTreePath string // Should return the root dir, empty string, since there are no subdirs in this repo - for _, deletedFile := range []string{ - "dir1/dir2/dir3/file.txt", - "file.txt", - } { - treePath := GetClosestParentWithFiles(deletedFile, commit) - assert.Equal(t, expectedTreePath, treePath) - } + repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1}) + t.Run("getUniquePatchBranchName", func(t *testing.T) { + branchName := getUniquePatchBranchName(t.Context(), "user2", repo) + assert.Equal(t, "user2-patch-1", branchName) + }) + t.Run("getClosestParentWithFiles", func(t *testing.T) { + gitRepo, _ := gitrepo.OpenRepository(git.DefaultContext, repo) + defer gitRepo.Close() + treePath := getClosestParentWithFiles(gitRepo, "sub-home-md-img-check", "docs/foo/bar") + assert.Equal(t, "docs", treePath) + treePath = getClosestParentWithFiles(gitRepo, "sub-home-md-img-check", "any/other") + assert.Empty(t, treePath) + }) } diff --git a/routers/web/repo/editor_uploader.go b/routers/web/repo/editor_uploader.go new file mode 100644 index 0000000000..1ce9a1aca4 --- /dev/null +++ b/routers/web/repo/editor_uploader.go @@ -0,0 +1,61 @@ +// Copyright 2025 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package repo + +import ( + "net/http" + + repo_model "code.gitea.io/gitea/models/repo" + "code.gitea.io/gitea/modules/setting" + "code.gitea.io/gitea/modules/util" + "code.gitea.io/gitea/services/context" + "code.gitea.io/gitea/services/context/upload" + files_service "code.gitea.io/gitea/services/repository/files" +) + +// UploadFileToServer upload file to server file dir not git +func UploadFileToServer(ctx *context.Context) { + file, header, err := ctx.Req.FormFile("file") + if err != nil { + ctx.ServerError("FormFile", err) + return + } + defer file.Close() + + buf := make([]byte, 1024) + n, _ := util.ReadAtMost(file, buf) + if n > 0 { + buf = buf[:n] + } + + err = upload.Verify(buf, header.Filename, setting.Repository.Upload.AllowedTypes) + if err != nil { + ctx.HTTPError(http.StatusBadRequest, err.Error()) + return + } + + name := files_service.CleanGitTreePath(header.Filename) + if len(name) == 0 { + ctx.HTTPError(http.StatusBadRequest, "Upload file name is invalid") + return + } + + uploaded, err := repo_model.NewUpload(ctx, name, buf, file) + if err != nil { + ctx.ServerError("NewUpload", err) + return + } + + ctx.JSON(http.StatusOK, map[string]string{"uuid": uploaded.UUID}) +} + +// RemoveUploadFileFromServer remove file from server file dir +func RemoveUploadFileFromServer(ctx *context.Context) { + fileUUID := ctx.FormString("file") + if err := repo_model.DeleteUploadByUUID(ctx, fileUUID); err != nil { + ctx.ServerError("DeleteUploadByUUID", err) + return + } + ctx.Status(http.StatusNoContent) +} diff --git a/routers/web/repo/editor_util.go b/routers/web/repo/editor_util.go new file mode 100644 index 0000000000..f910f0bd40 --- /dev/null +++ b/routers/web/repo/editor_util.go @@ -0,0 +1,110 @@ +// Copyright 2025 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package repo + +import ( + "context" + "fmt" + "path" + "strings" + + git_model "code.gitea.io/gitea/models/git" + repo_model "code.gitea.io/gitea/models/repo" + user_model "code.gitea.io/gitea/models/user" + "code.gitea.io/gitea/modules/git" + "code.gitea.io/gitea/modules/json" + "code.gitea.io/gitea/modules/log" + repo_module "code.gitea.io/gitea/modules/repository" + context_service "code.gitea.io/gitea/services/context" +) + +// getUniquePatchBranchName Gets a unique branch name for a new patch branch +// It will be in the form of <username>-patch-<num> where <num> is the first branch of this format +// that doesn't already exist. If we exceed 1000 tries or an error is thrown, we just return "" so the user has to +// type in the branch name themselves (will be an empty field) +func getUniquePatchBranchName(ctx context.Context, prefixName string, repo *repo_model.Repository) string { + prefix := prefixName + "-patch-" + for i := 1; i <= 1000; i++ { + branchName := fmt.Sprintf("%s%d", prefix, i) + if exist, err := git_model.IsBranchExist(ctx, repo.ID, branchName); err != nil { + log.Error("getUniquePatchBranchName: %v", err) + return "" + } else if !exist { + return branchName + } + } + return "" +} + +// getClosestParentWithFiles Recursively gets the closest path of parent in a tree that has files when a file in a tree is +// deleted. It returns "" for the tree root if no parents other than the root have files. +func getClosestParentWithFiles(gitRepo *git.Repository, branchName, originTreePath string) string { + var f func(treePath string, commit *git.Commit) string + f = func(treePath string, commit *git.Commit) string { + if treePath == "" || treePath == "." { + return "" + } + // see if the tree has entries + if tree, err := commit.SubTree(treePath); err != nil { + return f(path.Dir(treePath), commit) // failed to get the tree, going up a dir + } else if entries, err := tree.ListEntries(); err != nil || len(entries) == 0 { + return f(path.Dir(treePath), commit) // no files in this dir, going up a dir + } + return treePath + } + commit, err := gitRepo.GetBranchCommit(branchName) // must get the commit again to get the latest change + if err != nil { + log.Error("GetBranchCommit: %v", err) + return "" + } + return f(originTreePath, commit) +} + +// getContextRepoEditorConfig returns the editorconfig JSON string for given treePath or "null" +func getContextRepoEditorConfig(ctx *context_service.Context, treePath string) string { + ec, _, err := ctx.Repo.GetEditorconfig() + if err == nil { + def, err := ec.GetDefinitionForFilename(treePath) + if err == nil { + jsonStr, _ := json.Marshal(def) + return string(jsonStr) + } + } + return "null" +} + +// getParentTreeFields returns list of parent tree names and corresponding tree paths based on given treePath. +// eg: []{"a", "b", "c"}, []{"a", "a/b", "a/b/c"} +// or: []{""}, []{""} for the root treePath +func getParentTreeFields(treePath string) (treeNames, treePaths []string) { + treeNames = strings.Split(treePath, "/") + treePaths = make([]string, len(treeNames)) + for i := range treeNames { + treePaths[i] = strings.Join(treeNames[:i+1], "/") + } + return treeNames, treePaths +} + +// getUniqueRepositoryName Gets a unique repository name for a user +// It will append a -<num> postfix if the name is already taken +func getUniqueRepositoryName(ctx context.Context, ownerID int64, name string) string { + uniqueName := name + for i := 1; i < 1000; i++ { + _, err := repo_model.GetRepositoryByName(ctx, ownerID, uniqueName) + if err != nil || repo_model.IsErrRepoNotExist(err) { + return uniqueName + } + uniqueName = fmt.Sprintf("%s-%d", name, i) + i++ + } + return "" +} + +func editorPushBranchToForkedRepository(ctx context.Context, doer *user_model.User, baseRepo *repo_model.Repository, baseBranchName string, targetRepo *repo_model.Repository, targetBranchName string) error { + return git.Push(ctx, baseRepo.RepoPath(), git.PushOptions{ + Remote: targetRepo.RepoPath(), + Branch: baseBranchName + ":" + targetBranchName, + Env: repo_module.PushingEnvironment(doer, targetRepo), + }) +} diff --git a/routers/web/repo/fork.go b/routers/web/repo/fork.go index 9f5cda10c2..c2694e540f 100644 --- a/routers/web/repo/fork.go +++ b/routers/web/repo/fork.go @@ -189,17 +189,25 @@ func ForkPost(ctx *context.Context) { } } - repo, err := repo_service.ForkRepository(ctx, ctx.Doer, ctxUser, repo_service.ForkRepoOptions{ + repo := ForkRepoTo(ctx, ctxUser, repo_service.ForkRepoOptions{ BaseRepo: forkRepo, Name: form.RepoName, Description: form.Description, SingleBranch: form.ForkSingleBranch, }) + if ctx.Written() { + return + } + ctx.JSONRedirect(ctxUser.HomeLink() + "/" + url.PathEscape(repo.Name)) +} + +func ForkRepoTo(ctx *context.Context, owner *user_model.User, forkOpts repo_service.ForkRepoOptions) *repo_model.Repository { + repo, err := repo_service.ForkRepository(ctx, ctx.Doer, owner, forkOpts) if err != nil { ctx.Data["Err_RepoName"] = true switch { case repo_model.IsErrReachLimitOfRepo(err): - maxCreationLimit := ctxUser.MaxCreationLimit() + maxCreationLimit := owner.MaxCreationLimit() msg := ctx.TrN(maxCreationLimit, "repo.form.reach_limit_of_creation_1", "repo.form.reach_limit_of_creation_n", maxCreationLimit) ctx.JSONError(msg) case repo_model.IsErrRepoAlreadyExist(err): @@ -224,9 +232,7 @@ func ForkPost(ctx *context.Context) { default: ctx.ServerError("ForkPost", err) } - return + return nil } - - log.Trace("Repository forked[%d]: %s/%s", forkRepo.ID, ctxUser.Name, repo.Name) - ctx.JSONRedirect(ctxUser.HomeLink() + "/" + url.PathEscape(repo.Name)) + return repo } diff --git a/routers/web/repo/patch.go b/routers/web/repo/patch.go deleted file mode 100644 index 3ffd8f89c4..0000000000 --- a/routers/web/repo/patch.go +++ /dev/null @@ -1,126 +0,0 @@ -// Copyright 2021 The Gitea Authors. All rights reserved. -// SPDX-License-Identifier: MIT - -package repo - -import ( - "net/http" - "strings" - - git_model "code.gitea.io/gitea/models/git" - "code.gitea.io/gitea/models/unit" - "code.gitea.io/gitea/modules/setting" - "code.gitea.io/gitea/modules/templates" - "code.gitea.io/gitea/modules/util" - "code.gitea.io/gitea/modules/web" - "code.gitea.io/gitea/services/context" - "code.gitea.io/gitea/services/forms" - "code.gitea.io/gitea/services/repository/files" -) - -const ( - tplPatchFile templates.TplName = "repo/editor/patch" -) - -// NewDiffPatch render create patch page -func NewDiffPatch(ctx *context.Context) { - canCommit := renderCommitRights(ctx) - - ctx.Data["PageIsPatch"] = true - - ctx.Data["commit_summary"] = "" - ctx.Data["commit_message"] = "" - if canCommit { - ctx.Data["commit_choice"] = frmCommitChoiceDirect - } else { - ctx.Data["commit_choice"] = frmCommitChoiceNewBranch - } - ctx.Data["new_branch_name"] = GetUniquePatchBranchName(ctx) - ctx.Data["last_commit"] = ctx.Repo.CommitID - ctx.Data["LineWrapExtensions"] = strings.Join(setting.Repository.Editor.LineWrapExtensions, ",") - ctx.Data["BranchLink"] = ctx.Repo.RepoLink + "/src/" + ctx.Repo.RefTypeNameSubURL() - - ctx.HTML(http.StatusOK, tplPatchFile) -} - -// NewDiffPatchPost response for sending patch page -func NewDiffPatchPost(ctx *context.Context) { - form := web.GetForm(ctx).(*forms.EditRepoFileForm) - - canCommit := renderCommitRights(ctx) - branchName := ctx.Repo.BranchName - if form.CommitChoice == frmCommitChoiceNewBranch { - branchName = form.NewBranchName - } - ctx.Data["PageIsPatch"] = true - ctx.Data["BranchLink"] = ctx.Repo.RepoLink + "/src/" + ctx.Repo.RefTypeNameSubURL() - ctx.Data["FileContent"] = form.Content - ctx.Data["commit_summary"] = form.CommitSummary - ctx.Data["commit_message"] = form.CommitMessage - ctx.Data["commit_choice"] = form.CommitChoice - ctx.Data["new_branch_name"] = form.NewBranchName - ctx.Data["last_commit"] = ctx.Repo.CommitID - ctx.Data["LineWrapExtensions"] = strings.Join(setting.Repository.Editor.LineWrapExtensions, ",") - - if ctx.HasError() { - ctx.HTML(http.StatusOK, tplPatchFile) - return - } - - // Cannot commit to an existing branch if user doesn't have rights - if branchName == ctx.Repo.BranchName && !canCommit { - ctx.Data["Err_NewBranchName"] = true - ctx.Data["commit_choice"] = frmCommitChoiceNewBranch - ctx.RenderWithErr(ctx.Tr("repo.editor.cannot_commit_to_protected_branch", branchName), tplEditFile, &form) - return - } - - // CommitSummary is optional in the web form, if empty, give it a default message based on add or update - // `message` will be both the summary and message combined - message := strings.TrimSpace(form.CommitSummary) - if len(message) == 0 { - message = ctx.Locale.TrString("repo.editor.patch") - } - - form.CommitMessage = strings.TrimSpace(form.CommitMessage) - if len(form.CommitMessage) > 0 { - message += "\n\n" + form.CommitMessage - } - - gitCommitter, valid := WebGitOperationGetCommitChosenEmailIdentity(ctx, form.CommitEmail) - if !valid { - ctx.Data["Err_CommitEmail"] = true - ctx.RenderWithErr(ctx.Tr("repo.editor.invalid_commit_email"), tplPatchFile, &form) - return - } - - fileResponse, err := files.ApplyDiffPatch(ctx, ctx.Repo.Repository, ctx.Doer, &files.ApplyDiffPatchOptions{ - LastCommitID: form.LastCommit, - OldBranch: ctx.Repo.BranchName, - NewBranch: branchName, - Message: message, - Content: strings.ReplaceAll(form.Content.Value(), "\r", ""), - Author: gitCommitter, - Committer: gitCommitter, - }) - if err != nil { - if git_model.IsErrBranchAlreadyExists(err) { - // User has specified a branch that already exists - branchErr := err.(git_model.ErrBranchAlreadyExists) - ctx.Data["Err_NewBranchName"] = true - ctx.RenderWithErr(ctx.Tr("repo.editor.branch_already_exists", branchErr.BranchName), tplEditFile, &form) - return - } else if files.IsErrCommitIDDoesNotMatch(err) { - ctx.RenderWithErr(ctx.Tr("repo.editor.file_changed_while_editing", ctx.Repo.RepoLink+"/compare/"+form.LastCommit+"..."+ctx.Repo.CommitID), tplPatchFile, &form) - return - } - ctx.RenderWithErr(ctx.Tr("repo.editor.fail_to_apply_patch", err), tplPatchFile, &form) - return - } - - if form.CommitChoice == frmCommitChoiceNewBranch && ctx.Repo.Repository.UnitEnabled(ctx, unit.TypePullRequests) { - ctx.Redirect(ctx.Repo.RepoLink + "/compare/" + util.PathEscapeSegments(ctx.Repo.BranchName) + "..." + util.PathEscapeSegments(form.NewBranchName)) - } else { - ctx.Redirect(ctx.Repo.RepoLink + "/commit/" + fileResponse.Commit.SHA) - } -} diff --git a/routers/web/repo/view.go b/routers/web/repo/view.go index 68fa17bf07..f0d90f9533 100644 --- a/routers/web/repo/view.go +++ b/routers/web/repo/view.go @@ -394,9 +394,10 @@ func Forks(ctx *context.Context) { } pager := context.NewPagination(int(total), pageSize, page, 5) + ctx.Data["ShowRepoOwnerAvatar"] = true + ctx.Data["ShowRepoOwnerOnList"] = true ctx.Data["Page"] = pager - - ctx.Data["Forks"] = forks + ctx.Data["Repos"] = forks ctx.HTML(http.StatusOK, tplForks) } diff --git a/routers/web/repo/view_file.go b/routers/web/repo/view_file.go index ec0ad02828..5606a8e6ec 100644 --- a/routers/web/repo/view_file.go +++ b/routers/web/repo/view_file.go @@ -290,7 +290,7 @@ func prepareToRenderFile(ctx *context.Context, entry *git.TreeEntry) { func prepareToRenderButtons(ctx *context.Context, lfsLock *git_model.LFSLock) { // archived or mirror repository, the buttons should not be shown - if ctx.Repo.Repository.IsArchived || !ctx.Repo.Repository.CanEnableEditor() { + if !ctx.Repo.Repository.CanEnableEditor() { return } @@ -302,7 +302,9 @@ func prepareToRenderButtons(ctx *context.Context, lfsLock *git_model.LFSLock) { } if !ctx.Repo.CanWriteToBranch(ctx, ctx.Doer, ctx.Repo.BranchName) { + ctx.Data["CanEditFile"] = true ctx.Data["EditFileTooltip"] = ctx.Tr("repo.editor.fork_before_edit") + ctx.Data["CanDeleteFile"] = true ctx.Data["DeleteFileTooltip"] = ctx.Tr("repo.editor.must_have_write_access") return } diff --git a/routers/web/repo/view_readme.go b/routers/web/repo/view_readme.go index 7af6ad450e..4ce22d79db 100644 --- a/routers/web/repo/view_readme.go +++ b/routers/web/repo/view_readme.go @@ -212,7 +212,7 @@ func prepareToRenderReadmeFile(ctx *context.Context, subfolder string, readmeFil ctx.Data["EscapeStatus"], ctx.Data["FileContent"] = charset.EscapeControlHTML(template.HTML(contentEscaped), ctx.Locale) } - if !fInfo.isLFSFile && ctx.Repo.CanEnableEditor(ctx, ctx.Doer) { + if !fInfo.isLFSFile && ctx.Repo.Repository.CanEnableEditor() { ctx.Data["CanEditReadmeFile"] = true } } diff --git a/routers/web/repo/wiki.go b/routers/web/repo/wiki.go index a1e10c380d..69858c9692 100644 --- a/routers/web/repo/wiki.go +++ b/routers/web/repo/wiki.go @@ -7,6 +7,7 @@ package repo import ( "bytes" gocontext "context" + "html/template" "io" "net/http" "net/url" @@ -61,9 +62,9 @@ func MustEnableWiki(ctx *context.Context) { return } - unit, err := ctx.Repo.Repository.GetUnit(ctx, unit.TypeExternalWiki) + repoUnit, err := ctx.Repo.Repository.GetUnit(ctx, unit.TypeExternalWiki) if err == nil { - ctx.Redirect(unit.ExternalWikiConfig().ExternalWikiURL) + ctx.Redirect(repoUnit.ExternalWikiConfig().ExternalWikiURL) return } } @@ -95,7 +96,7 @@ func findEntryForFile(commit *git.Commit, target string) (*git.TreeEntry, error) } func findWikiRepoCommit(ctx *context.Context) (*git.Repository, *git.Commit, error) { - wikiGitRepo, errGitRepo := gitrepo.OpenRepository(ctx, ctx.Repo.Repository.WikiStorageRepo()) + wikiGitRepo, errGitRepo := gitrepo.RepositoryFromRequestContextOrOpen(ctx, ctx.Repo.Repository.WikiStorageRepo()) if errGitRepo != nil { ctx.ServerError("OpenRepository", errGitRepo) return nil, nil, errGitRepo @@ -178,23 +179,17 @@ func wikiContentsByName(ctx *context.Context, commit *git.Commit, wikiName wiki_ } func renderViewPage(ctx *context.Context) (*git.Repository, *git.TreeEntry) { - wikiRepo, commit, err := findWikiRepoCommit(ctx) + wikiGitRepo, commit, err := findWikiRepoCommit(ctx) if err != nil { - if wikiRepo != nil { - wikiRepo.Close() - } if !git.IsErrNotExist(err) { ctx.ServerError("GetBranchCommit", err) } return nil, nil } - // Get page list. + // get the wiki pages list. entries, err := commit.ListEntries() if err != nil { - if wikiRepo != nil { - wikiRepo.Close() - } ctx.ServerError("ListEntries", err) return nil, nil } @@ -208,9 +203,6 @@ func renderViewPage(ctx *context.Context) (*git.Repository, *git.TreeEntry) { if repo_model.IsErrWikiInvalidFileName(err) { continue } - if wikiRepo != nil { - wikiRepo.Close() - } ctx.ServerError("WikiFilenameToName", err) return nil, nil } else if wikiName == "_Sidebar" || wikiName == "_Footer" { @@ -249,58 +241,26 @@ func renderViewPage(ctx *context.Context) (*git.Repository, *git.TreeEntry) { ctx.Redirect(util.URLJoin(ctx.Repo.RepoLink, "wiki/raw", string(pageName))) } if entry == nil || ctx.Written() { - if wikiRepo != nil { - wikiRepo.Close() - } return nil, nil } - // get filecontent + // get page content data := wikiContentsByEntry(ctx, entry) if ctx.Written() { - if wikiRepo != nil { - wikiRepo.Close() - } return nil, nil } - var sidebarContent []byte - if !isSideBar { - sidebarContent, _, _, _ = wikiContentsByName(ctx, commit, "_Sidebar") - if ctx.Written() { - if wikiRepo != nil { - wikiRepo.Close() - } - return nil, nil - } - } else { - sidebarContent = data - } - - var footerContent []byte - if !isFooter { - footerContent, _, _, _ = wikiContentsByName(ctx, commit, "_Footer") - if ctx.Written() { - if wikiRepo != nil { - wikiRepo.Close() - } - return nil, nil - } - } else { - footerContent = data - } - rctx := renderhelper.NewRenderContextRepoWiki(ctx, ctx.Repo.Repository) - buf := &strings.Builder{} - renderFn := func(data []byte) (escaped *charset.EscapeStatus, output string, err error) { + renderFn := func(data []byte) (escaped *charset.EscapeStatus, output template.HTML, err error) { + buf := &strings.Builder{} markupRd, markupWr := io.Pipe() defer markupWr.Close() done := make(chan struct{}) go func() { // We allow NBSP here this is rendered escaped, _ = charset.EscapeControlReader(markupRd, buf, ctx.Locale, charset.RuneNBSP) - output = buf.String() + output = template.HTML(buf.String()) buf.Reset() close(done) }() @@ -311,75 +271,61 @@ func renderViewPage(ctx *context.Context) (*git.Repository, *git.TreeEntry) { return escaped, output, err } - ctx.Data["EscapeStatus"], ctx.Data["content"], err = renderFn(data) + ctx.Data["EscapeStatus"], ctx.Data["WikiContentHTML"], err = renderFn(data) if err != nil { - if wikiRepo != nil { - wikiRepo.Close() - } ctx.ServerError("Render", err) return nil, nil } if rctx.SidebarTocNode != nil { - sb := &strings.Builder{} - err = markdown.SpecializedMarkdown(rctx).Renderer().Render(sb, nil, rctx.SidebarTocNode) - if err != nil { + sb := strings.Builder{} + if err = markdown.SpecializedMarkdown(rctx).Renderer().Render(&sb, nil, rctx.SidebarTocNode); err != nil { log.Error("Failed to render wiki sidebar TOC: %v", err) - } else { - ctx.Data["sidebarTocContent"] = sb.String() } + ctx.Data["WikiSidebarTocHTML"] = templates.SanitizeHTML(sb.String()) } if !isSideBar { - buf.Reset() - ctx.Data["sidebarEscapeStatus"], ctx.Data["sidebarContent"], err = renderFn(sidebarContent) + sidebarContent, _, _, _ := wikiContentsByName(ctx, commit, "_Sidebar") + if ctx.Written() { + return nil, nil + } + ctx.Data["WikiSidebarEscapeStatus"], ctx.Data["WikiSidebarHTML"], err = renderFn(sidebarContent) if err != nil { - if wikiRepo != nil { - wikiRepo.Close() - } ctx.ServerError("Render", err) return nil, nil } - ctx.Data["sidebarPresent"] = sidebarContent != nil - } else { - ctx.Data["sidebarPresent"] = false } if !isFooter { - buf.Reset() - ctx.Data["footerEscapeStatus"], ctx.Data["footerContent"], err = renderFn(footerContent) + footerContent, _, _, _ := wikiContentsByName(ctx, commit, "_Footer") + if ctx.Written() { + return nil, nil + } + ctx.Data["WikiFooterEscapeStatus"], ctx.Data["WikiFooterHTML"], err = renderFn(footerContent) if err != nil { - if wikiRepo != nil { - wikiRepo.Close() - } ctx.ServerError("Render", err) return nil, nil } - ctx.Data["footerPresent"] = footerContent != nil - } else { - ctx.Data["footerPresent"] = false } // get commit count - wiki revisions - commitsCount, _ := wikiRepo.FileCommitsCount(ctx.Repo.Repository.DefaultWikiBranch, pageFilename) + commitsCount, _ := wikiGitRepo.FileCommitsCount(ctx.Repo.Repository.DefaultWikiBranch, pageFilename) ctx.Data["CommitCount"] = commitsCount - return wikiRepo, entry + return wikiGitRepo, entry } func renderRevisionPage(ctx *context.Context) (*git.Repository, *git.TreeEntry) { - wikiRepo, commit, err := findWikiRepoCommit(ctx) + wikiGitRepo, commit, err := findWikiRepoCommit(ctx) if err != nil { - if wikiRepo != nil { - wikiRepo.Close() - } if !git.IsErrNotExist(err) { ctx.ServerError("GetBranchCommit", err) } return nil, nil } - // get requested pagename + // get requested page name pageName := wiki_service.WebPathFromRequest(ctx.PathParamRaw("*")) if len(pageName) == 0 { pageName = "Home" @@ -394,50 +340,35 @@ func renderRevisionPage(ctx *context.Context) (*git.Repository, *git.TreeEntry) ctx.Data["Username"] = ctx.Repo.Owner.Name ctx.Data["Reponame"] = ctx.Repo.Repository.Name - // lookup filename in wiki - get filecontent, gitTree entry , real filename - data, entry, pageFilename, noEntry := wikiContentsByName(ctx, commit, pageName) + // lookup filename in wiki - get page content, gitTree entry , real filename + _, entry, pageFilename, noEntry := wikiContentsByName(ctx, commit, pageName) if noEntry { ctx.Redirect(ctx.Repo.RepoLink + "/wiki/?action=_pages") } if entry == nil || ctx.Written() { - if wikiRepo != nil { - wikiRepo.Close() - } return nil, nil } - ctx.Data["content"] = string(data) - ctx.Data["sidebarPresent"] = false - ctx.Data["sidebarContent"] = "" - ctx.Data["footerPresent"] = false - ctx.Data["footerContent"] = "" - // get commit count - wiki revisions - commitsCount, _ := wikiRepo.FileCommitsCount(ctx.Repo.Repository.DefaultWikiBranch, pageFilename) + commitsCount, _ := wikiGitRepo.FileCommitsCount(ctx.Repo.Repository.DefaultWikiBranch, pageFilename) ctx.Data["CommitCount"] = commitsCount // get page page := max(ctx.FormInt("page"), 1) // get Commit Count - commitsHistory, err := wikiRepo.CommitsByFileAndRange( + commitsHistory, err := wikiGitRepo.CommitsByFileAndRange( git.CommitsByFileAndRangeOptions{ Revision: ctx.Repo.Repository.DefaultWikiBranch, File: pageFilename, Page: page, }) if err != nil { - if wikiRepo != nil { - wikiRepo.Close() - } ctx.ServerError("CommitsByFileAndRange", err) return nil, nil } ctx.Data["Commits"], err = git_service.ConvertFromGitCommit(ctx, commitsHistory, ctx.Repo.Repository) if err != nil { - if wikiRepo != nil { - wikiRepo.Close() - } ctx.ServerError("ConvertFromGitCommit", err) return nil, nil } @@ -446,16 +377,11 @@ func renderRevisionPage(ctx *context.Context) (*git.Repository, *git.TreeEntry) pager.AddParamFromRequest(ctx.Req) ctx.Data["Page"] = pager - return wikiRepo, entry + return wikiGitRepo, entry } func renderEditPage(ctx *context.Context) { - wikiRepo, commit, err := findWikiRepoCommit(ctx) - defer func() { - if wikiRepo != nil { - _ = wikiRepo.Close() - } - }() + _, commit, err := findWikiRepoCommit(ctx) if err != nil { if !git.IsErrNotExist(err) { ctx.ServerError("GetBranchCommit", err) @@ -463,7 +389,7 @@ func renderEditPage(ctx *context.Context) { return } - // get requested pagename + // get requested page name pageName := wiki_service.WebPathFromRequest(ctx.PathParamRaw("*")) if len(pageName) == 0 { pageName = "Home" @@ -487,17 +413,13 @@ func renderEditPage(ctx *context.Context) { return } - // get filecontent + // get wiki page content data := wikiContentsByEntry(ctx, entry) if ctx.Written() { return } - ctx.Data["content"] = string(data) - ctx.Data["sidebarPresent"] = false - ctx.Data["sidebarContent"] = "" - ctx.Data["footerPresent"] = false - ctx.Data["footerContent"] = "" + ctx.Data["WikiEditContent"] = string(data) } // WikiPost renders post of wiki page @@ -559,12 +481,7 @@ func Wiki(ctx *context.Context) { return } - wikiRepo, entry := renderViewPage(ctx) - defer func() { - if wikiRepo != nil { - wikiRepo.Close() - } - }() + wikiGitRepo, entry := renderViewPage(ctx) if ctx.Written() { return } @@ -580,7 +497,7 @@ func Wiki(ctx *context.Context) { ctx.Data["FormatWarning"] = ext + " rendering is not supported at the moment. Rendered as Markdown." } // Get last change information. - lastCommit, err := wikiRepo.GetCommitByPath(wikiPath) + lastCommit, err := wikiGitRepo.GetCommitByPath(wikiPath) if err != nil { ctx.ServerError("GetCommitByPath", err) return @@ -600,13 +517,7 @@ func WikiRevision(ctx *context.Context) { return } - wikiRepo, entry := renderRevisionPage(ctx) - defer func() { - if wikiRepo != nil { - wikiRepo.Close() - } - }() - + wikiGitRepo, entry := renderRevisionPage(ctx) if ctx.Written() { return } @@ -618,7 +529,7 @@ func WikiRevision(ctx *context.Context) { // Get last change information. wikiPath := entry.Name() - lastCommit, err := wikiRepo.GetCommitByPath(wikiPath) + lastCommit, err := wikiGitRepo.GetCommitByPath(wikiPath) if err != nil { ctx.ServerError("GetCommitByPath", err) return @@ -638,12 +549,7 @@ func WikiPages(ctx *context.Context) { ctx.Data["Title"] = ctx.Tr("repo.wiki.pages") ctx.Data["CanWriteWiki"] = ctx.Repo.CanWrite(unit.TypeWiki) && !ctx.Repo.Repository.IsArchived - wikiRepo, commit, err := findWikiRepoCommit(ctx) - defer func() { - if wikiRepo != nil { - _ = wikiRepo.Close() - } - }() + _, commit, err := findWikiRepoCommit(ctx) if err != nil { ctx.Redirect(ctx.Repo.RepoLink + "/wiki") return @@ -697,13 +603,7 @@ func WikiPages(ctx *context.Context) { // WikiRaw outputs raw blob requested by user (image for example) func WikiRaw(ctx *context.Context) { - wikiRepo, commit, err := findWikiRepoCommit(ctx) - defer func() { - if wikiRepo != nil { - wikiRepo.Close() - } - }() - + _, commit, err := findWikiRepoCommit(ctx) if err != nil { if git.IsErrNotExist(err) { ctx.NotFound(nil) diff --git a/routers/web/repo/wiki_test.go b/routers/web/repo/wiki_test.go index 73f9970a07..59bf6ed79b 100644 --- a/routers/web/repo/wiki_test.go +++ b/routers/web/repo/wiki_test.go @@ -164,7 +164,7 @@ func TestEditWiki(t *testing.T) { EditWiki(ctx) assert.Equal(t, http.StatusOK, ctx.Resp.WrittenStatus()) assert.EqualValues(t, "Home", ctx.Data["Title"]) - assert.Equal(t, wikiContent(t, ctx.Repo.Repository, "Home"), ctx.Data["content"]) + assert.Equal(t, wikiContent(t, ctx.Repo.Repository, "Home"), ctx.Data["WikiEditContent"]) ctx, _ = contexttest.MockContext(t, "user2/repo1/wiki/jpeg.jpg?action=_edit") ctx.SetPathParam("*", "jpeg.jpg") diff --git a/routers/web/user/profile.go b/routers/web/user/profile.go index f6d50cf5fe..d7052914b6 100644 --- a/routers/web/user/profile.go +++ b/routers/web/user/profile.go @@ -197,6 +197,7 @@ func prepareUserProfileTabData(ctx *context.Context, profileDbRepo *repo_model.R total = int(count) case "stars": ctx.Data["PageIsProfileStarList"] = true + ctx.Data["ShowRepoOwnerOnList"] = true repos, count, err = repo_model.SearchRepository(ctx, repo_model.SearchRepoOptions{ ListOptions: db.ListOptions{ PageSize: pagingNum, diff --git a/routers/web/web.go b/routers/web/web.go index a54f96ec68..4b5d68b260 100644 --- a/routers/web/web.go +++ b/routers/web/web.go @@ -964,7 +964,8 @@ func registerWebRoutes(m *web.Router) { addSettingsVariablesRoutes() }, actions.MustEnableActions) - m.Methods("GET,POST", "/delete", org.SettingsDelete) + m.Post("/rename", web.Bind(forms.RenameOrgForm{}), org.SettingsRenamePost) + m.Post("/delete", org.SettingsDeleteOrgPost) m.Group("/packages", func() { m.Get("", org.Packages) @@ -1312,26 +1313,38 @@ func registerWebRoutes(m *web.Router) { }, reqSignIn, context.RepoAssignment, context.RepoMustNotBeArchived()) // end "/{username}/{reponame}": create or edit issues, pulls, labels, milestones - m.Group("/{username}/{reponame}", func() { // repo code + m.Group("/{username}/{reponame}", func() { // repo code (at least "code reader") m.Group("", func() { m.Group("", func() { - m.Post("/_preview/*", web.Bind(forms.EditPreviewDiffForm{}), repo.DiffPreviewPost) - m.Combo("/_edit/*").Get(repo.EditFile). - Post(web.Bind(forms.EditRepoFileForm{}), repo.EditFilePost) - m.Combo("/_new/*").Get(repo.NewFile). - Post(web.Bind(forms.EditRepoFileForm{}), repo.NewFilePost) - m.Combo("/_delete/*").Get(repo.DeleteFile). - Post(web.Bind(forms.DeleteRepoFileForm{}), repo.DeleteFilePost) - m.Combo("/_upload/*", repo.MustBeAbleToUpload).Get(repo.UploadFile). - Post(web.Bind(forms.UploadRepoFileForm{}), repo.UploadFilePost) - m.Combo("/_diffpatch/*").Get(repo.NewDiffPatch). - Post(web.Bind(forms.EditRepoFileForm{}), repo.NewDiffPatchPost) - m.Combo("/_cherrypick/{sha:([a-f0-9]{7,64})}/*").Get(repo.CherryPick). - Post(web.Bind(forms.CherryPickForm{}), repo.CherryPickPost) - }, context.RepoRefByType(git.RefTypeBranch), context.CanWriteToBranch(), repo.WebGitOperationCommonData) + // "GET" requests only need "code reader" permission, "POST" requests need "code writer" permission. + // Because reader can "fork and edit" + canWriteToBranch := context.CanWriteToBranch() + m.Post("/_preview/*", repo.DiffPreviewPost) // read-only, fine with "code reader" + m.Post("/_fork/*", repo.ForkToEditPost) // read-only, fork to own repo, fine with "code reader" + + // the path params are used in PrepareCommitFormOptions to construct the correct form action URL + m.Combo("/{editor_action:_edit}/*"). + Get(repo.EditFile). + Post(web.Bind(forms.EditRepoFileForm{}), canWriteToBranch, repo.EditFilePost) + m.Combo("/{editor_action:_new}/*"). + Get(repo.EditFile). + Post(web.Bind(forms.EditRepoFileForm{}), canWriteToBranch, repo.EditFilePost) + m.Combo("/{editor_action:_delete}/*"). + Get(repo.DeleteFile). + Post(web.Bind(forms.DeleteRepoFileForm{}), canWriteToBranch, repo.DeleteFilePost) + m.Combo("/{editor_action:_upload}/*", repo.MustBeAbleToUpload). + Get(repo.UploadFile). + Post(web.Bind(forms.UploadRepoFileForm{}), canWriteToBranch, repo.UploadFilePost) + m.Combo("/{editor_action:_diffpatch}/*"). + Get(repo.NewDiffPatch). + Post(web.Bind(forms.EditRepoFileForm{}), canWriteToBranch, repo.NewDiffPatchPost) + m.Combo("/{editor_action:_cherrypick}/{sha:([a-f0-9]{7,64})}/*"). + Get(repo.CherryPick). + Post(web.Bind(forms.CherryPickForm{}), canWriteToBranch, repo.CherryPickPost) + }, context.RepoRefByType(git.RefTypeBranch), repo.WebGitOperationCommonData) m.Group("", func() { m.Post("/upload-file", repo.UploadFileToServer) - m.Post("/upload-remove", web.Bind(forms.RemoveUploadFileForm{}), repo.RemoveUploadFileFromServer) + m.Post("/upload-remove", repo.RemoveUploadFileFromServer) }, repo.MustBeAbleToUpload, reqRepoCodeWriter) }, repo.MustBeEditable, context.RepoMustNotBeArchived()) |