Merge branch 'main' into patch1

This commit is contained in:
Giteabot 2025-06-23 06:18:44 +08:00 committed by GitHub
commit b014b139fc
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
55 changed files with 1024 additions and 903 deletions

View File

@ -653,7 +653,7 @@ func (repo *Repository) AllowsPulls(ctx context.Context) bool {
// CanEnableEditor returns true if repository meets the requirements of web editor.
func (repo *Repository) CanEnableEditor() bool {
return !repo.IsMirror
return !repo.IsMirror && !repo.IsArchived
}
// DescriptionHTML does special handles to description and return HTML string.

View File

@ -4,6 +4,7 @@
package markup
import (
"html/template"
"io"
"net/url"
"regexp"
@ -92,9 +93,9 @@ func (st *Sanitizer) createDefaultPolicy() *bluemonday.Policy {
return policy
}
// Sanitize takes a string that contains a HTML fragment or document and applies policy whitelist.
func Sanitize(s string) string {
return GetDefaultSanitizer().defaultPolicy.Sanitize(s)
// Sanitize use default sanitizer policy to sanitize a string
func Sanitize(s string) template.HTML {
return template.HTML(GetDefaultSanitizer().defaultPolicy.Sanitize(s))
}
// SanitizeReader sanitizes a Reader

View File

@ -69,6 +69,6 @@ func TestSanitizer(t *testing.T) {
}
for i := 0; i < len(testCases); i += 2 {
assert.Equal(t, testCases[i+1], Sanitize(testCases[i]))
assert.Equal(t, testCases[i+1], string(Sanitize(testCases[i])))
}
}

View File

@ -176,9 +176,9 @@ func safeHTML(s any) template.HTML {
panic(fmt.Sprintf("unexpected type %T", s))
}
// SanitizeHTML sanitizes the input by pre-defined markdown rules
// SanitizeHTML sanitizes the input by default sanitization rules.
func SanitizeHTML(s string) template.HTML {
return template.HTML(markup.Sanitize(s))
return markup.Sanitize(s)
}
func htmlEscape(s any) template.HTML {

View File

@ -6,6 +6,7 @@ package web
import (
"net/http"
"regexp"
"slices"
"strings"
"code.gitea.io/gitea/modules/container"
@ -36,11 +37,21 @@ func (g *RouterPathGroup) ServeHTTP(resp http.ResponseWriter, req *http.Request)
g.r.chiRouter.NotFoundHandler().ServeHTTP(resp, req)
}
type RouterPathGroupPattern struct {
re *regexp.Regexp
params []routerPathParam
middlewares []any
}
// MatchPath matches the request method, and uses regexp to match the path.
// The pattern uses "<...>" to define path parameters, for example: "/<name>" (different from chi router)
// It is only designed to resolve some special cases which chi router can't handle.
// The pattern uses "<...>" to define path parameters, for example, "/<name>" (different from chi router)
// It is only designed to resolve some special cases that chi router can't handle.
// For most cases, it shouldn't be used because it needs to iterate all rules to find the matched one (inefficient).
func (g *RouterPathGroup) MatchPath(methods, pattern string, h ...any) {
g.MatchPattern(methods, g.PatternRegexp(pattern), h...)
}
func (g *RouterPathGroup) MatchPattern(methods string, pattern *RouterPathGroupPattern, h ...any) {
g.matchers = append(g.matchers, newRouterPathMatcher(methods, pattern, h...))
}
@ -96,8 +107,8 @@ func isValidMethod(name string) bool {
return false
}
func newRouterPathMatcher(methods, pattern string, h ...any) *routerPathMatcher {
middlewares, handlerFunc := wrapMiddlewareAndHandler(nil, h)
func newRouterPathMatcher(methods string, patternRegexp *RouterPathGroupPattern, h ...any) *routerPathMatcher {
middlewares, handlerFunc := wrapMiddlewareAndHandler(patternRegexp.middlewares, h)
p := &routerPathMatcher{methods: make(container.Set[string]), middlewares: middlewares, handlerFunc: handlerFunc}
for method := range strings.SplitSeq(methods, ",") {
method = strings.TrimSpace(method)
@ -106,19 +117,25 @@ func newRouterPathMatcher(methods, pattern string, h ...any) *routerPathMatcher
}
p.methods.Add(method)
}
p.re, p.params = patternRegexp.re, patternRegexp.params
return p
}
func patternRegexp(pattern string, h ...any) *RouterPathGroupPattern {
p := &RouterPathGroupPattern{middlewares: slices.Clone(h)}
re := []byte{'^'}
lastEnd := 0
for lastEnd < len(pattern) {
start := strings.IndexByte(pattern[lastEnd:], '<')
if start == -1 {
re = append(re, pattern[lastEnd:]...)
re = append(re, regexp.QuoteMeta(pattern[lastEnd:])...)
break
}
end := strings.IndexByte(pattern[lastEnd+start:], '>')
if end == -1 {
panic("invalid pattern: " + pattern)
}
re = append(re, pattern[lastEnd:lastEnd+start]...)
re = append(re, regexp.QuoteMeta(pattern[lastEnd:lastEnd+start])...)
partName, partExp, _ := strings.Cut(pattern[lastEnd+start+1:lastEnd+start+end], ":")
lastEnd += start + end + 1
@ -140,7 +157,10 @@ func newRouterPathMatcher(methods, pattern string, h ...any) *routerPathMatcher
p.params = append(p.params, param)
}
re = append(re, '$')
reStr := string(re)
p.re = regexp.MustCompile(reStr)
p.re = regexp.MustCompile(string(re))
return p
}
func (g *RouterPathGroup) PatternRegexp(pattern string, h ...any) *RouterPathGroupPattern {
return patternRegexp(pattern, h...)
}

View File

@ -34,7 +34,7 @@ func TestPathProcessor(t *testing.T) {
testProcess := func(pattern, uri string, expectedPathParams map[string]string) {
chiCtx := chi.NewRouteContext()
chiCtx.RouteMethod = "GET"
p := newRouterPathMatcher("GET", pattern, http.NotFound)
p := newRouterPathMatcher("GET", patternRegexp(pattern), http.NotFound)
assert.True(t, p.matchPath(chiCtx, uri), "use pattern %s to process uri %s", pattern, uri)
assert.Equal(t, expectedPathParams, chiURLParamsToMap(chiCtx), "use pattern %s to process uri %s", pattern, uri)
}
@ -56,18 +56,20 @@ func TestRouter(t *testing.T) {
recorder.Body = buff
type resultStruct struct {
method string
pathParams map[string]string
handlerMark string
method string
pathParams map[string]string
handlerMarks []string
}
var res resultStruct
var res resultStruct
h := func(optMark ...string) func(resp http.ResponseWriter, req *http.Request) {
mark := util.OptionalArg(optMark, "")
return func(resp http.ResponseWriter, req *http.Request) {
res.method = req.Method
res.pathParams = chiURLParamsToMap(chi.RouteContext(req.Context()))
res.handlerMark = mark
if mark != "" {
res.handlerMarks = append(res.handlerMarks, mark)
}
}
}
@ -77,6 +79,8 @@ func TestRouter(t *testing.T) {
if stop := req.FormValue("stop"); stop != "" && (mark == "" || mark == stop) {
h(stop)(resp, req)
resp.WriteHeader(http.StatusOK)
} else if mark != "" {
res.handlerMarks = append(res.handlerMarks, mark)
}
}
}
@ -108,7 +112,7 @@ func TestRouter(t *testing.T) {
m.Delete("", h())
})
m.PathGroup("/*", func(g *RouterPathGroup) {
g.MatchPath("GET", `/<dir:*>/<file:[a-z]{1,2}>`, stopMark("s2"), h("match-path"))
g.MatchPattern("GET", g.PatternRegexp(`/<dir:*>/<file:[a-z]{1,2}>`, stopMark("s2")), stopMark("s3"), h("match-path"))
}, stopMark("s1"))
})
})
@ -126,31 +130,31 @@ func TestRouter(t *testing.T) {
}
t.Run("RootRouter", func(t *testing.T) {
testRoute(t, "GET /the-user/the-repo/other", resultStruct{method: "GET", handlerMark: "not-found:/"})
testRoute(t, "GET /the-user/the-repo/other", resultStruct{method: "GET", handlerMarks: []string{"not-found:/"}})
testRoute(t, "GET /the-user/the-repo/pulls", resultStruct{
method: "GET",
pathParams: map[string]string{"username": "the-user", "reponame": "the-repo", "type": "pulls"},
handlerMark: "list-issues-b",
method: "GET",
pathParams: map[string]string{"username": "the-user", "reponame": "the-repo", "type": "pulls"},
handlerMarks: []string{"list-issues-b"},
})
testRoute(t, "GET /the-user/the-repo/issues/123", resultStruct{
method: "GET",
pathParams: map[string]string{"username": "the-user", "reponame": "the-repo", "type": "issues", "index": "123"},
handlerMark: "view-issue",
method: "GET",
pathParams: map[string]string{"username": "the-user", "reponame": "the-repo", "type": "issues", "index": "123"},
handlerMarks: []string{"view-issue"},
})
testRoute(t, "GET /the-user/the-repo/issues/123?stop=hijack", resultStruct{
method: "GET",
pathParams: map[string]string{"username": "the-user", "reponame": "the-repo", "type": "issues", "index": "123"},
handlerMark: "hijack",
method: "GET",
pathParams: map[string]string{"username": "the-user", "reponame": "the-repo", "type": "issues", "index": "123"},
handlerMarks: []string{"hijack"},
})
testRoute(t, "POST /the-user/the-repo/issues/123/update", resultStruct{
method: "POST",
pathParams: map[string]string{"username": "the-user", "reponame": "the-repo", "index": "123"},
handlerMark: "update-issue",
method: "POST",
pathParams: map[string]string{"username": "the-user", "reponame": "the-repo", "index": "123"},
handlerMarks: []string{"update-issue"},
})
})
t.Run("Sub Router", func(t *testing.T) {
testRoute(t, "GET /api/v1/other", resultStruct{method: "GET", handlerMark: "not-found:/api/v1"})
testRoute(t, "GET /api/v1/other", resultStruct{method: "GET", handlerMarks: []string{"not-found:/api/v1"}})
testRoute(t, "GET /api/v1/repos/the-user/the-repo/branches", resultStruct{
method: "GET",
pathParams: map[string]string{"username": "the-user", "reponame": "the-repo"},
@ -179,31 +183,37 @@ func TestRouter(t *testing.T) {
t.Run("MatchPath", func(t *testing.T) {
testRoute(t, "GET /api/v1/repos/the-user/the-repo/branches/d1/d2/fn", resultStruct{
method: "GET",
pathParams: map[string]string{"username": "the-user", "reponame": "the-repo", "*": "d1/d2/fn", "dir": "d1/d2", "file": "fn"},
handlerMark: "match-path",
method: "GET",
pathParams: map[string]string{"username": "the-user", "reponame": "the-repo", "*": "d1/d2/fn", "dir": "d1/d2", "file": "fn"},
handlerMarks: []string{"s1", "s2", "s3", "match-path"},
})
testRoute(t, "GET /api/v1/repos/the-user/the-repo/branches/d1%2fd2/fn", resultStruct{
method: "GET",
pathParams: map[string]string{"username": "the-user", "reponame": "the-repo", "*": "d1%2fd2/fn", "dir": "d1%2fd2", "file": "fn"},
handlerMark: "match-path",
method: "GET",
pathParams: map[string]string{"username": "the-user", "reponame": "the-repo", "*": "d1%2fd2/fn", "dir": "d1%2fd2", "file": "fn"},
handlerMarks: []string{"s1", "s2", "s3", "match-path"},
})
testRoute(t, "GET /api/v1/repos/the-user/the-repo/branches/d1/d2/000", resultStruct{
method: "GET",
pathParams: map[string]string{"reponame": "the-repo", "username": "the-user", "*": "d1/d2/000"},
handlerMark: "not-found:/api/v1",
method: "GET",
pathParams: map[string]string{"reponame": "the-repo", "username": "the-user", "*": "d1/d2/000"},
handlerMarks: []string{"s1", "not-found:/api/v1"},
})
testRoute(t, "GET /api/v1/repos/the-user/the-repo/branches/d1/d2/fn?stop=s1", resultStruct{
method: "GET",
pathParams: map[string]string{"username": "the-user", "reponame": "the-repo", "*": "d1/d2/fn"},
handlerMark: "s1",
method: "GET",
pathParams: map[string]string{"username": "the-user", "reponame": "the-repo", "*": "d1/d2/fn"},
handlerMarks: []string{"s1"},
})
testRoute(t, "GET /api/v1/repos/the-user/the-repo/branches/d1/d2/fn?stop=s2", resultStruct{
method: "GET",
pathParams: map[string]string{"username": "the-user", "reponame": "the-repo", "*": "d1/d2/fn", "dir": "d1/d2", "file": "fn"},
handlerMark: "s2",
method: "GET",
pathParams: map[string]string{"username": "the-user", "reponame": "the-repo", "*": "d1/d2/fn", "dir": "d1/d2", "file": "fn"},
handlerMarks: []string{"s1", "s2"},
})
testRoute(t, "GET /api/v1/repos/the-user/the-repo/branches/d1/d2/fn?stop=s3", resultStruct{
method: "GET",
pathParams: map[string]string{"username": "the-user", "reponame": "the-repo", "*": "d1/d2/fn", "dir": "d1/d2", "file": "fn"},
handlerMarks: []string{"s1", "s2", "s3"},
})
})
}

View File

@ -1399,6 +1399,13 @@ editor.revert = Revert %s onto:
editor.failed_to_commit = Failed to commit changes.
editor.failed_to_commit_summary = Error Message:
editor.fork_create = Fork Repository to Propose Changes
editor.fork_create_description = You can not edit this repository directly. Instead you can create a fork, make edits and create a pull request.
editor.fork_edit_description = You can not edit this repository directly. The changes will be written to your fork <b>%s</b>, so you can create a pull request.
editor.fork_not_editable = You have forked this repository but your fork is not editable.
editor.fork_failed_to_push_branch = Failed to push branch %s to your repository.
editor.fork_branch_exists = Branch "%s" already exists in your fork, please choose a new branch name.
commits.desc = Browse source code change history.
commits.commits = Commits
commits.no_commits = No commits in common. "%s" and "%s" have entirely different histories.

View File

@ -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)

View File

@ -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())

View File

@ -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
}

View File

@ -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

View File

@ -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) {

View File

@ -16,6 +16,7 @@ import (
"code.gitea.io/gitea/modules/charset"
"code.gitea.io/gitea/modules/git"
"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"
@ -39,7 +40,7 @@ const (
editorCommitChoiceNewBranch string = "commit-to-new-branch"
)
func prepareEditorCommitFormOptions(ctx *context.Context, editorAction string) {
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))
@ -47,18 +48,28 @@ func prepareEditorCommitFormOptions(ctx *context.Context, editorAction string) {
redirectTo += "?" + ctx.Req.URL.RawQuery
}
ctx.Redirect(redirectTo)
return
return nil
}
commitFormBehaviors, err := ctx.Repo.PrepareCommitFormBehaviors(ctx, ctx.Doer)
commitFormOptions, err := context.PrepareCommitFormOptions(ctx, ctx.Doer, ctx.Repo.Repository, ctx.Repo.Permission, ctx.Repo.RefFullName)
if err != nil {
ctx.ServerError("PrepareCommitFormBehaviors", err)
return
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["CommitFormBehaviors"] = commitFormBehaviors
ctx.Data["CommitFormOptions"] = commitFormOptions
// for online editor
ctx.Data["PreviewableExtensions"] = strings.Join(markup.PreviewableExtensions(), ",")
@ -69,25 +80,27 @@ func prepareEditorCommitFormOptions(ctx *context.Context, editorAction string) {
// form fields
ctx.Data["commit_summary"] = ""
ctx.Data["commit_message"] = ""
ctx.Data["commit_choice"] = util.Iif(commitFormBehaviors.CanCommitToBranch, editorCommitChoiceDirect, editorCommitChoiceNewBranch)
ctx.Data["new_branch_name"] = getUniquePatchBranchName(ctx, ctx.Doer.LowerName, ctx.Repo.Repository)
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(treePath)
ctx.Data["TreeNames"], ctx.Data["TreePaths"] = getParentTreeFields(strings.TrimPrefix(treePath, "/"))
}
type parsedEditorCommitForm[T any] struct {
form T
commonForm *forms.CommitCommonForm
CommitFormBehaviors *context.CommitFormBehaviors
TargetBranchName string
GitCommitter *files_service.IdentityOptions
type preparedEditorCommitForm[T any] struct {
form T
commonForm *forms.CommitCommonForm
CommitFormOptions *context.CommitFormOptions
OldBranchName string
NewBranchName string
GitCommitter *files_service.IdentityOptions
}
func (f *parsedEditorCommitForm[T]) GetCommitMessage(defaultCommitMessage string) string {
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
@ -95,7 +108,7 @@ func (f *parsedEditorCommitForm[T]) GetCommitMessage(defaultCommitMessage string
return commitMessage
}
func parseEditorCommitSubmittedForm[T forms.CommitCommonFormInterface](ctx *context.Context) *parsedEditorCommitForm[T] {
func prepareEditorCommitSubmittedForm[T forms.CommitCommonFormInterface](ctx *context.Context) *preparedEditorCommitForm[T] {
form := web.GetForm(ctx).(T)
if ctx.HasError() {
ctx.JSONError(ctx.GetErrMsg())
@ -105,15 +118,22 @@ func parseEditorCommitSubmittedForm[T forms.CommitCommonFormInterface](ctx *cont
commonForm := form.GetCommitCommonForm()
commonForm.TreePath = files_service.CleanGitTreePath(commonForm.TreePath)
commitFormBehaviors, err := ctx.Repo.PrepareCommitFormBehaviors(ctx, ctx.Doer)
commitFormOptions, err := context.PrepareCommitFormOptions(ctx, ctx.Doer, ctx.Repo.Repository, ctx.Repo.Permission, ctx.Repo.RefFullName)
if err != nil {
ctx.ServerError("PrepareCommitFormBehaviors", 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
targetBranchName := util.Iif(commonForm.CommitChoice == editorCommitChoiceNewBranch, commonForm.NewBranchName, ctx.Repo.BranchName)
if targetBranchName == ctx.Repo.BranchName && !commitFormBehaviors.CanCommitToBranch {
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
}
@ -125,28 +145,63 @@ func parseEditorCommitSubmittedForm[T forms.CommitCommonFormInterface](ctx *cont
return nil
}
return &parsedEditorCommitForm[T]{
form: form,
commonForm: commonForm,
CommitFormBehaviors: commitFormBehaviors,
TargetBranchName: targetBranchName,
GitCommitter: gitCommitter,
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[T any](ctx *context.Context, parsed *parsedEditorCommitForm[T], treePath string) {
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, baseBranch, headBranch := ctx.Repo.Repository, ctx.Repo.BranchName, parsed.TargetBranchName
if repo.UnitEnabled(ctx, unit.TypePullRequests) {
redirectToPullRequest = true
} else if parsed.CommitFormBehaviors.CanCreateBasePullRequest {
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.JSONRedirect(repo.Link() + "/compare/" + util.PathEscapeSegments(baseBranch) + "..." + util.PathEscapeSegments(headBranch))
@ -154,11 +209,9 @@ func redirectForCommitChoice[T any](ctx *context.Context, parsed *parsedEditorCo
}
}
returnURI := ctx.FormString("return_uri")
if returnURI == "" || !httplib.IsCurrentGiteaSiteURL(ctx, returnURI) {
returnURI = util.URLJoin(ctx.Repo.RepoLink, "src/branch", util.PathEscapeSegments(parsed.TargetBranchName), util.PathEscapeSegments(treePath))
}
ctx.JSONRedirect(returnURI)
// redirect to the newly updated file
redirectTo := util.URLJoin(ctx.Repo.RepoLink, "src/branch", util.PathEscapeSegments(parsed.NewBranchName), util.PathEscapeSegments(treePath))
ctx.JSONRedirect(redirectTo)
}
func editFileOpenExisting(ctx *context.Context) (prefetch []byte, dataRc io.ReadCloser, fInfo *fileInfo) {
@ -268,7 +321,7 @@ func EditFile(ctx *context.Context) {
func EditFilePost(ctx *context.Context) {
editorAction := ctx.PathParam("editor_action")
isNewFile := editorAction == "_new"
parsed := parseEditorCommitSubmittedForm[*forms.EditRepoFileForm](ctx)
parsed := prepareEditorCommitSubmittedForm[*forms.EditRepoFileForm](ctx)
if ctx.Written() {
return
}
@ -292,8 +345,8 @@ func EditFilePost(ctx *context.Context) {
_, err := files_service.ChangeRepoFiles(ctx, ctx.Repo.Repository, ctx.Doer, &files_service.ChangeRepoFilesOptions{
LastCommitID: parsed.form.LastCommit,
OldBranch: ctx.Repo.BranchName,
NewBranch: parsed.TargetBranchName,
OldBranch: parsed.OldBranchName,
NewBranch: parsed.NewBranchName,
Message: parsed.GetCommitMessage(defaultCommitMessage),
Files: []*files_service.ChangeRepoFile{
{
@ -308,7 +361,7 @@ func EditFilePost(ctx *context.Context) {
Committer: parsed.GitCommitter,
})
if err != nil {
editorHandleFileOperationError(ctx, parsed.TargetBranchName, err)
editorHandleFileOperationError(ctx, parsed.NewBranchName, err)
return
}
@ -327,7 +380,7 @@ func DeleteFile(ctx *context.Context) {
// DeleteFilePost response for deleting file
func DeleteFilePost(ctx *context.Context) {
parsed := parseEditorCommitSubmittedForm[*forms.DeleteRepoFileForm](ctx)
parsed := prepareEditorCommitSubmittedForm[*forms.DeleteRepoFileForm](ctx)
if ctx.Written() {
return
}
@ -335,8 +388,8 @@ func DeleteFilePost(ctx *context.Context) {
treePath := ctx.Repo.TreePath
_, err := files_service.ChangeRepoFiles(ctx, ctx.Repo.Repository, ctx.Doer, &files_service.ChangeRepoFilesOptions{
LastCommitID: parsed.form.LastCommit,
OldBranch: ctx.Repo.BranchName,
NewBranch: parsed.TargetBranchName,
OldBranch: parsed.OldBranchName,
NewBranch: parsed.NewBranchName,
Files: []*files_service.ChangeRepoFile{
{
Operation: "delete",
@ -349,29 +402,29 @@ func DeleteFilePost(ctx *context.Context) {
Committer: parsed.GitCommitter,
})
if err != nil {
editorHandleFileOperationError(ctx, parsed.TargetBranchName, err)
editorHandleFileOperationError(ctx, parsed.NewBranchName, err)
return
}
ctx.Flash.Success(ctx.Tr("repo.editor.file_delete_success", treePath))
redirectTreePath := getClosestParentWithFiles(ctx.Repo.GitRepo, parsed.TargetBranchName, treePath)
redirectTreePath := getClosestParentWithFiles(ctx.Repo.GitRepo, parsed.NewBranchName, treePath)
redirectForCommitChoice(ctx, parsed, redirectTreePath)
}
func UploadFile(ctx *context.Context) {
ctx.Data["PageIsUpload"] = true
upload.AddUploadContext(ctx, "repo")
prepareTreePathFieldsAndPaths(ctx, ctx.Repo.TreePath)
prepareEditorCommitFormOptions(ctx, "_upload")
opts := prepareEditorCommitFormOptions(ctx, "_upload")
if ctx.Written() {
return
}
upload.AddUploadContextForRepo(ctx, opts.TargetRepo)
ctx.HTML(http.StatusOK, tplUploadFile)
}
func UploadFilePost(ctx *context.Context) {
parsed := parseEditorCommitSubmittedForm[*forms.UploadRepoFileForm](ctx)
parsed := prepareEditorCommitSubmittedForm[*forms.UploadRepoFileForm](ctx)
if ctx.Written() {
return
}
@ -379,8 +432,8 @@ func UploadFilePost(ctx *context.Context) {
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: ctx.Repo.BranchName,
NewBranch: parsed.TargetBranchName,
OldBranch: parsed.OldBranchName,
NewBranch: parsed.NewBranchName,
TreePath: parsed.form.TreePath,
Message: parsed.GetCommitMessage(defaultCommitMessage),
Files: parsed.form.Files,
@ -389,7 +442,7 @@ func UploadFilePost(ctx *context.Context) {
Committer: parsed.GitCommitter,
})
if err != nil {
editorHandleFileOperationError(ctx, parsed.TargetBranchName, err)
editorHandleFileOperationError(ctx, parsed.NewBranchName, err)
return
}
redirectForCommitChoice(ctx, parsed, parsed.form.TreePath)

View File

@ -25,7 +25,7 @@ func NewDiffPatch(ctx *context.Context) {
// NewDiffPatchPost response for sending patch page
func NewDiffPatchPost(ctx *context.Context) {
parsed := parseEditorCommitSubmittedForm[*forms.EditRepoFileForm](ctx)
parsed := prepareEditorCommitSubmittedForm[*forms.EditRepoFileForm](ctx)
if ctx.Written() {
return
}
@ -33,8 +33,8 @@ func NewDiffPatchPost(ctx *context.Context) {
defaultCommitMessage := ctx.Locale.TrString("repo.editor.patch")
_, err := files.ApplyDiffPatch(ctx, ctx.Repo.Repository, ctx.Doer, &files.ApplyDiffPatchOptions{
LastCommitID: parsed.form.LastCommit,
OldBranch: ctx.Repo.BranchName,
NewBranch: parsed.TargetBranchName,
OldBranch: parsed.OldBranchName,
NewBranch: parsed.NewBranchName,
Message: parsed.GetCommitMessage(defaultCommitMessage),
Content: strings.ReplaceAll(parsed.form.Content.Value(), "\r\n", "\n"),
Author: parsed.GitCommitter,
@ -44,7 +44,7 @@ func NewDiffPatchPost(ctx *context.Context) {
err = util.ErrorWrapLocale(err, "repo.editor.fail_to_apply_patch")
}
if err != nil {
editorHandleFileOperationError(ctx, parsed.TargetBranchName, err)
editorHandleFileOperationError(ctx, parsed.NewBranchName, err)
return
}
redirectForCommitChoice(ctx, parsed, parsed.form.TreePath)

View File

@ -45,7 +45,7 @@ func CherryPick(ctx *context.Context) {
func CherryPickPost(ctx *context.Context) {
fromCommitID := ctx.PathParam("sha")
parsed := parseEditorCommitSubmittedForm[*forms.CherryPickForm](ctx)
parsed := prepareEditorCommitSubmittedForm[*forms.CherryPickForm](ctx)
if ctx.Written() {
return
}
@ -53,8 +53,8 @@ func CherryPickPost(ctx *context.Context) {
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: ctx.Repo.BranchName,
NewBranch: parsed.TargetBranchName,
OldBranch: parsed.OldBranchName,
NewBranch: parsed.NewBranchName,
Message: parsed.GetCommitMessage(defaultCommitMessage),
Author: parsed.GitCommitter,
Committer: parsed.GitCommitter,
@ -78,7 +78,7 @@ func CherryPickPost(ctx *context.Context) {
}
}
if err != nil {
editorHandleFileOperationError(ctx, parsed.TargetBranchName, err)
editorHandleFileOperationError(ctx, parsed.NewBranchName, err)
return
}
}

View File

@ -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
}

View File

@ -11,9 +11,11 @@ import (
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"
)
@ -83,3 +85,26 @@ func getParentTreeFields(treePath string) (treeNames, treePaths []string) {
}
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),
})
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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
}
}

View File

@ -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)

View File

@ -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")

View File

@ -1313,23 +1313,35 @@ 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/*", repo.DiffPreviewPost)
m.Combo("/{editor_action:_edit}/*").Get(repo.EditFile).
Post(web.Bind(forms.EditRepoFileForm{}), repo.EditFilePost)
m.Combo("/{editor_action:_new}/*").Get(repo.EditFile).
Post(web.Bind(forms.EditRepoFileForm{}), repo.EditFilePost)
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", repo.RemoveUploadFileFromServer)

View File

@ -71,11 +71,6 @@ func (r *Repository) CanWriteToBranch(ctx context.Context, user *user_model.User
return issues_model.CanMaintainerWriteToBranch(ctx, r.Permission, branch, user)
}
// CanEnableEditor returns true if repository is editable and user has proper access level.
func (r *Repository) CanEnableEditor(ctx context.Context, user *user_model.User) bool {
return r.RefFullName.IsBranch() && r.CanWriteToBranch(ctx, user, r.BranchName) && r.Repository.CanEnableEditor() && !r.Repository.IsArchived
}
// CanCreateBranch returns true if repository is editable and user has proper access level.
func (r *Repository) CanCreateBranch() bool {
return r.Permission.CanWrite(unit_model.TypeCode) && r.Repository.CanCreateBranch()
@ -94,9 +89,13 @@ func RepoMustNotBeArchived() func(ctx *Context) {
}
}
type CommitFormBehaviors struct {
type CommitFormOptions struct {
NeedFork bool
TargetRepo *repo_model.Repository
TargetFormAction string
WillSubmitToFork bool
CanCommitToBranch bool
EditorEnabled bool
UserCanPush bool
RequireSigned bool
WillSign bool
@ -106,51 +105,84 @@ type CommitFormBehaviors struct {
CanCreateBasePullRequest bool
}
func (r *Repository) PrepareCommitFormBehaviors(ctx *Context, doer *user_model.User) (*CommitFormBehaviors, error) {
protectedBranch, err := git_model.GetFirstMatchProtectedBranchRule(ctx, r.Repository.ID, r.BranchName)
func PrepareCommitFormOptions(ctx *Context, doer *user_model.User, targetRepo *repo_model.Repository, doerRepoPerm access_model.Permission, refName git.RefName) (*CommitFormOptions, error) {
if !refName.IsBranch() {
// it shouldn't happen because middleware already checks
return nil, util.NewInvalidArgumentErrorf("ref %q is not a branch", refName)
}
originRepo := targetRepo
branchName := refName.ShortName()
// TODO: CanMaintainerWriteToBranch is a bad name, but it really does what "CanWriteToBranch" does
if !issues_model.CanMaintainerWriteToBranch(ctx, doerRepoPerm, branchName, doer) {
targetRepo = repo_model.GetForkedRepo(ctx, doer.ID, targetRepo.ID)
if targetRepo == nil {
return &CommitFormOptions{NeedFork: true}, nil
}
// now, we get our own forked repo; it must be writable by us.
}
submitToForkedRepo := targetRepo.ID != originRepo.ID
err := targetRepo.GetBaseRepo(ctx)
if err != nil {
return nil, err
}
userCanPush := true
requireSigned := false
if protectedBranch != nil {
protectedBranch.Repo = r.Repository
userCanPush = protectedBranch.CanUserPush(ctx, doer)
requireSigned = protectedBranch.RequireSignedCommits
}
sign, keyID, _, err := asymkey_service.SignCRUDAction(ctx, r.Repository.RepoPath(), doer, r.Repository.RepoPath(), git.BranchPrefix+r.BranchName)
canEnableEditor := r.CanEnableEditor(ctx, doer)
canCommit := canEnableEditor && userCanPush
if requireSigned {
canCommit = canCommit && sign
}
wontSignReason := ""
protectedBranch, err := git_model.GetFirstMatchProtectedBranchRule(ctx, targetRepo.ID, branchName)
if err != nil {
if asymkey_service.IsErrWontSign(err) {
wontSignReason = string(err.(*asymkey_service.ErrWontSign).Reason)
err = nil
} else {
wontSignReason = "error"
}
return nil, err
}
canPushWithProtection := true
protectionRequireSigned := false
if protectedBranch != nil {
protectedBranch.Repo = targetRepo
canPushWithProtection = protectedBranch.CanUserPush(ctx, doer)
protectionRequireSigned = protectedBranch.RequireSignedCommits
}
canCreateBasePullRequest := ctx.Repo.Repository.BaseRepo != nil && ctx.Repo.Repository.BaseRepo.UnitEnabled(ctx, unit_model.TypePullRequests)
canCreatePullRequest := ctx.Repo.Repository.UnitEnabled(ctx, unit_model.TypePullRequests) || canCreateBasePullRequest
willSign, signKeyID, _, err := asymkey_service.SignCRUDAction(ctx, targetRepo.RepoPath(), doer, targetRepo.RepoPath(), refName.String())
wontSignReason := ""
if asymkey_service.IsErrWontSign(err) {
wontSignReason = string(err.(*asymkey_service.ErrWontSign).Reason)
} else if err != nil {
return nil, err
}
return &CommitFormBehaviors{
CanCommitToBranch: canCommit,
EditorEnabled: canEnableEditor,
UserCanPush: userCanPush,
RequireSigned: requireSigned,
WillSign: sign,
SigningKey: keyID,
canCommitToBranch := !submitToForkedRepo /* same repo */ && targetRepo.CanEnableEditor() && canPushWithProtection
if protectionRequireSigned {
canCommitToBranch = canCommitToBranch && willSign
}
canCreateBasePullRequest := targetRepo.BaseRepo != nil && targetRepo.BaseRepo.UnitEnabled(ctx, unit_model.TypePullRequests)
canCreatePullRequest := targetRepo.UnitEnabled(ctx, unit_model.TypePullRequests) || canCreateBasePullRequest
opts := &CommitFormOptions{
TargetRepo: targetRepo,
WillSubmitToFork: submitToForkedRepo,
CanCommitToBranch: canCommitToBranch,
UserCanPush: canPushWithProtection,
RequireSigned: protectionRequireSigned,
WillSign: willSign,
SigningKey: signKeyID,
WontSignReason: wontSignReason,
CanCreatePullRequest: canCreatePullRequest,
CanCreateBasePullRequest: canCreateBasePullRequest,
}, err
}
editorAction := ctx.PathParam("editor_action")
editorPathParamRemaining := util.PathEscapeSegments(branchName) + "/" + util.PathEscapeSegments(ctx.Repo.TreePath)
if submitToForkedRepo {
// there is only "default branch" in forked repo, we will use "from_base_branch" to get a new branch from base repo
editorPathParamRemaining = util.PathEscapeSegments(targetRepo.DefaultBranch) + "/" + util.PathEscapeSegments(ctx.Repo.TreePath) + "?from_base_branch=" + url.QueryEscape(branchName)
}
if editorAction == "_cherrypick" {
opts.TargetFormAction = targetRepo.Link() + "/" + editorAction + "/" + ctx.PathParam("sha") + "/" + editorPathParamRemaining
} else {
opts.TargetFormAction = targetRepo.Link() + "/" + editorAction + "/" + editorPathParamRemaining
}
if ctx.Req.URL.RawQuery != "" {
opts.TargetFormAction += util.Iif(strings.Contains(opts.TargetFormAction, "?"), "&", "?") + ctx.Req.URL.RawQuery
}
return opts, nil
}
// CanUseTimetracker returns whether a user can use the timetracker.

View File

@ -11,7 +11,9 @@ import (
"regexp"
"strings"
repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/reqctx"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/services/context"
)
@ -106,14 +108,17 @@ func AddUploadContext(ctx *context.Context, uploadType string) {
ctx.Data["UploadAccepts"] = strings.ReplaceAll(setting.Attachment.AllowedTypes, "|", ",")
ctx.Data["UploadMaxFiles"] = setting.Attachment.MaxFiles
ctx.Data["UploadMaxSize"] = setting.Attachment.MaxSize
case "repo":
ctx.Data["UploadUrl"] = ctx.Repo.RepoLink + "/upload-file"
ctx.Data["UploadRemoveUrl"] = ctx.Repo.RepoLink + "/upload-remove"
ctx.Data["UploadLinkUrl"] = ctx.Repo.RepoLink + "/upload-file"
ctx.Data["UploadAccepts"] = strings.ReplaceAll(setting.Repository.Upload.AllowedTypes, "|", ",")
ctx.Data["UploadMaxFiles"] = setting.Repository.Upload.MaxFiles
ctx.Data["UploadMaxSize"] = setting.Repository.Upload.FileMaxSize
default:
setting.PanicInDevOrTesting("Invalid upload type: %s", uploadType)
}
}
func AddUploadContextForRepo(ctx reqctx.RequestContext, repo *repo_model.Repository) {
ctxData, repoLink := ctx.GetData(), repo.Link()
ctxData["UploadUrl"] = repoLink + "/upload-file"
ctxData["UploadRemoveUrl"] = repoLink + "/upload-remove"
ctxData["UploadLinkUrl"] = repoLink + "/upload-file"
ctxData["UploadAccepts"] = strings.ReplaceAll(setting.Repository.Upload.AllowedTypes, "|", ",")
ctxData["UploadMaxFiles"] = setting.Repository.Upload.MaxFiles
ctxData["UploadMaxSize"] = setting.Repository.Upload.FileMaxSize
}

View File

@ -13,7 +13,7 @@ import (
container_module "code.gitea.io/gitea/modules/packages/container"
packages_service "code.gitea.io/gitea/services/packages"
digest "github.com/opencontainers/go-digest"
"github.com/opencontainers/go-digest"
)
// Cleanup removes expired container data

View File

@ -195,7 +195,7 @@ func (telegramConvertor) WorkflowJob(p *api.WorkflowJobPayload) (TelegramPayload
func createTelegramPayloadHTML(msgHTML string) TelegramPayload {
// https://core.telegram.org/bots/api#formatting-options
return TelegramPayload{
Message: strings.TrimSpace(markup.Sanitize(msgHTML)),
Message: strings.TrimSpace(string(markup.Sanitize(msgHTML))),
ParseMode: "HTML",
DisableWebPreview: true,
}

View File

@ -3,8 +3,9 @@
{{template "repo/header" .}}
<div class="ui container">
{{template "base/alert" .}}
<form class="ui edit form form-fetch-action" method="post" action="{{.RepoLink}}/_cherrypick/{{.FromCommitID}}/{{.BranchName | PathEscapeSegments}}">
<form class="ui edit form form-fetch-action" method="post" action="{{.CommitFormOptions.TargetFormAction}}">
{{.CsrfTokenHtml}}
{{template "repo/editor/common_top" .}}
<input type="hidden" name="revert" value="{{if eq .CherryPickType "revert"}}true{{else}}false{{end}}">
<div class="repo-editor-header">
<div class="breadcrumb">

View File

@ -1,11 +1,11 @@
<div class="commit-form-wrapper">
{{ctx.AvatarUtils.Avatar .SignedUser 40 "commit-avatar"}}
<div class="commit-form">
<h3>{{- if .CommitFormBehaviors.WillSign}}
<span title="{{ctx.Locale.Tr "repo.signing.will_sign" .CommitFormBehaviors.SigningKey}}">{{svg "octicon-lock" 24}}</span>
<h3>{{- if .CommitFormOptions.WillSign}}
<span title="{{ctx.Locale.Tr "repo.signing.will_sign" .CommitFormOptions.SigningKey}}">{{svg "octicon-lock" 24}}</span>
{{ctx.Locale.Tr "repo.editor.commit_signed_changes"}}
{{- else}}
<span title="{{ctx.Locale.Tr (printf "repo.signing.wont_sign.%s" .CommitFormBehaviors.WontSignReason)}}">{{svg "octicon-unlock" 24}}</span>
<span title="{{ctx.Locale.Tr (printf "repo.signing.wont_sign.%s" .CommitFormOptions.WontSignReason)}}">{{svg "octicon-unlock" 24}}</span>
{{ctx.Locale.Tr "repo.editor.commit_changes"}}
{{- end}}</h3>
<div class="field">
@ -22,17 +22,17 @@
</div>
<div class="quick-pull-choice js-quick-pull-choice">
<div class="field">
<div class="ui radio checkbox {{if not .CommitFormBehaviors.CanCommitToBranch}}disabled{{end}}">
<div class="ui radio checkbox {{if not .CommitFormOptions.CanCommitToBranch}}disabled{{end}}">
<input type="radio" class="js-quick-pull-choice-option" name="commit_choice" value="direct" data-button-text="{{ctx.Locale.Tr "repo.editor.commit_changes"}}" {{if eq .commit_choice "direct"}}checked{{end}}>
<label>
{{svg "octicon-git-commit"}}
{{ctx.Locale.Tr "repo.editor.commit_directly_to_this_branch" .BranchName}}
{{if not .CommitFormBehaviors.CanCommitToBranch}}
{{if not .CommitFormOptions.CanCommitToBranch}}
<div class="ui visible small warning message">
{{ctx.Locale.Tr "repo.editor.no_commit_to_branch"}}
<ul>
{{if not .CommitFormBehaviors.UserCanPush}}<li>{{ctx.Locale.Tr "repo.editor.user_no_push_to_branch"}}</li>{{end}}
{{if and .CommitFormBehaviors.RequireSigned (not .CommitFormBehaviors.WillSign)}}<li>{{ctx.Locale.Tr "repo.editor.require_signed_commit"}}</li>{{end}}
{{if not .CommitFormOptions.UserCanPush}}<li>{{ctx.Locale.Tr "repo.editor.user_no_push_to_branch"}}</li>{{end}}
{{if and .CommitFormOptions.RequireSigned (not .CommitFormOptions.WillSign)}}<li>{{ctx.Locale.Tr "repo.editor.require_signed_commit"}}</li>{{end}}
</ul>
</div>
{{end}}
@ -42,14 +42,14 @@
{{if and (not .Repository.IsEmpty) (not .IsEditingFileOnly)}}
<div class="field">
<div class="ui radio checkbox">
{{if .CommitFormBehaviors.CanCreatePullRequest}}
{{if .CommitFormOptions.CanCreatePullRequest}}
<input type="radio" class="js-quick-pull-choice-option" name="commit_choice" value="commit-to-new-branch" data-button-text="{{ctx.Locale.Tr "repo.editor.propose_file_change"}}" {{if eq .commit_choice "commit-to-new-branch"}}checked{{end}}>
{{else}}
<input type="radio" class="js-quick-pull-choice-option" name="commit_choice" value="commit-to-new-branch" data-button-text="{{ctx.Locale.Tr "repo.editor.commit_changes"}}" {{if eq .commit_choice "commit-to-new-branch"}}checked{{end}}>
{{end}}
<label>
{{svg "octicon-git-pull-request"}}
{{if .CommitFormBehaviors.CanCreatePullRequest}}
{{if .CommitFormOptions.CanCreatePullRequest}}
{{ctx.Locale.Tr "repo.editor.create_new_branch"}}
{{else}}
{{ctx.Locale.Tr "repo.editor.create_new_branch_np"}}

View File

@ -0,0 +1,6 @@
{{if .CommitFormOptions.WillSubmitToFork}}
<div class="ui blue message">
{{$repoLinkHTML := HTMLFormat `<a href="%s">%s</a>` .CommitFormOptions.TargetRepo.Link .CommitFormOptions.TargetRepo.FullName}}
{{ctx.Locale.Tr "repo.editor.fork_edit_description" $repoLinkHTML}}
</div>
{{end}}

View File

@ -3,8 +3,9 @@
{{template "repo/header" .}}
<div class="ui container">
{{template "base/alert" .}}
<form class="ui form form-fetch-action" method="post">
<form class="ui form form-fetch-action" method="post" action="{{.CommitFormOptions.TargetFormAction}}">
{{.CsrfTokenHtml}}
{{template "repo/editor/common_top" .}}
{{template "repo/editor/commit_form" .}}
</form>
</div>

View File

@ -3,11 +3,12 @@
{{template "repo/header" .}}
<div class="ui container">
{{template "base/alert" .}}
<form class="ui edit form form-fetch-action" method="post"
<form class="ui edit form form-fetch-action" method="post" action="{{.CommitFormOptions.TargetFormAction}}"
data-text-empty-confirm-header="{{ctx.Locale.Tr "repo.editor.commit_empty_file_header"}}"
data-text-empty-confirm-content="{{ctx.Locale.Tr "repo.editor.commit_empty_file_text"}}"
>
{{.CsrfTokenHtml}}
{{template "repo/editor/common_top" .}}
<div class="repo-editor-header">
{{template "repo/editor/common_breadcrumb" .}}
</div>

View File

@ -0,0 +1,18 @@
{{template "base/head" .}}
<div role="main" aria-label="{{.Title}}" class="page-content repository">
{{template "repo/header" .}}
<div class="ui container">
{{template "base/alert" .}}
<form class="ui form form-fetch-action" method="post" action="{{.RepoLink}}/_fork/{{.BranchName | PathEscapeSegments}}">
{{.CsrfTokenHtml}}
<div class="tw-text-center">
<div class="tw-my-[40px]">
<h3>{{ctx.Locale.Tr "repo.editor.fork_create"}}</h3>
<p>{{ctx.Locale.Tr "repo.editor.fork_create_description"}}</p>
</div>
<button class="ui primary button">{{ctx.Locale.Tr "repo.fork_repo"}}</button>
</div>
</form>
</div>
</div>
{{template "base/footer" .}}

View File

@ -3,11 +3,12 @@
{{template "repo/header" .}}
<div class="ui container">
{{template "base/alert" .}}
<form class="ui edit form form-fetch-action" method="post" action="{{.RepoLink}}/_diffpatch/{{.BranchName | PathEscapeSegments}}"
<form class="ui edit form form-fetch-action" method="post" action="{{.CommitFormOptions.TargetFormAction}}"
data-text-empty-confirm-header="{{ctx.Locale.Tr "repo.editor.commit_empty_file_header"}}"
data-text-empty-confirm-content="{{ctx.Locale.Tr "repo.editor.commit_empty_file_text"}}"
>
{{.CsrfTokenHtml}}
{{template "repo/editor/common_top" .}}
<div class="repo-editor-header">
<div class="breadcrumb">
{{ctx.Locale.Tr "repo.editor.patching"}}

View File

@ -3,8 +3,9 @@
{{template "repo/header" .}}
<div class="ui container">
{{template "base/alert" .}}
<form class="ui comment form form-fetch-action" method="post">
<form class="ui comment form form-fetch-action" method="post" action="{{.CommitFormOptions.TargetFormAction}}">
{{.CsrfTokenHtml}}
{{template "repo/editor/common_top" .}}
<div class="repo-editor-header">
{{template "repo/editor/common_breadcrumb" .}}
</div>

View File

@ -1,22 +1,22 @@
{{if .EscapeStatus}}
{{if .EscapeStatus.HasInvisible}}
<div class="ui warning message unicode-escape-prompt tw-text-left">
<div class="ui warning message unicode-escape-prompt">
<button class="btn close icon hide-panel" data-panel-closest=".message">{{svg "octicon-x" 16 "close inside"}}</button>
<div class="header">
{{ctx.Locale.Tr "repo.invisible_runes_header"}}
</div>
<p>{{ctx.Locale.Tr "repo.invisible_runes_description"}}</p>
<div>{{ctx.Locale.Tr "repo.invisible_runes_description"}}</div>
{{if .EscapeStatus.HasAmbiguous}}
<p>{{ctx.Locale.Tr "repo.ambiguous_runes_description"}}</p>
<div>{{ctx.Locale.Tr "repo.ambiguous_runes_description"}}</div>
{{end}}
</div>
{{else if .EscapeStatus.HasAmbiguous}}
<div class="ui warning message unicode-escape-prompt tw-text-left">
<div class="ui warning message unicode-escape-prompt">
<button class="btn close icon hide-panel" data-panel-closest=".message">{{svg "octicon-x" 16 "close inside"}}</button>
<div class="header">
{{ctx.Locale.Tr "repo.ambiguous_runes_header"}}
</div>
<p>{{ctx.Locale.Tr "repo.ambiguous_runes_description"}}</p>
<div>{{ctx.Locale.Tr "repo.ambiguous_runes_description"}}</div>
</div>
{{end}}
{{end}}

View File

@ -41,8 +41,8 @@
<a href="{{.Repository.Link}}/find/{{.RefTypeNameSubURL}}" class="ui compact basic button">{{ctx.Locale.Tr "repo.find_file.go_to_file"}}</a>
{{end}}
{{if and .CanWriteCode .RefFullName.IsBranch (not .Repository.IsMirror) (not .Repository.IsArchived) (not .IsViewFile)}}
<button class="ui dropdown basic compact jump button"{{if not .Repository.CanEnableEditor}} disabled{{end}}>
{{if and .RefFullName.IsBranch (not .IsViewFile)}}
<button class="ui dropdown basic compact jump button repo-add-file" {{if not .Repository.CanEnableEditor}}disabled{{end}}>
{{ctx.Locale.Tr "repo.editor.add_file"}}
{{svg "octicon-triangle-down" 14 "dropdown icon"}}
<div class="menu">

View File

@ -18,7 +18,7 @@
{{ctx.Locale.Tr "repo.wiki.page_name_desc"}}
</div>
{{$content := .content}}
{{$content := .WikiEditContent}}
{{if not .PageIsWikiEdit}}
{{$content = ctx.Locale.Tr "repo.wiki.welcome"}}
{{end}}

View File

@ -62,36 +62,34 @@
{{end}}
<div class="wiki-content-parts">
{{if .sidebarTocContent}}
{{if .WikiSidebarTocHTML}}
<div class="render-content markup wiki-content-sidebar wiki-content-toc">
{{.sidebarTocContent | SafeHTML}}
{{.WikiSidebarTocHTML}}
</div>
{{end}}
<div class="render-content markup wiki-content-main {{if or .sidebarTocContent .sidebarPresent}}with-sidebar{{end}}">
{{template "repo/unicode_escape_prompt" dict "EscapeStatus" .EscapeStatus "root" $}}
{{.content | SafeHTML}}
<div class="render-content markup wiki-content-main {{if or .WikiSidebarTocHTML .WikiSidebarHTML}}with-sidebar{{end}}">
{{template "repo/unicode_escape_prompt" dict "EscapeStatus" .EscapeStatus}}
{{.WikiContentHTML}}
</div>
{{if .sidebarPresent}}
{{if .WikiSidebarHTML}}
<div class="render-content markup wiki-content-sidebar">
{{if and .CanWriteWiki (not .Repository.IsMirror)}}
<a class="tw-float-right muted" href="{{.RepoLink}}/wiki/_Sidebar?action=_edit" aria-label="{{ctx.Locale.Tr "repo.wiki.edit_page_button"}}">{{svg "octicon-pencil"}}</a>
{{end}}
{{template "repo/unicode_escape_prompt" dict "EscapeStatus" .sidebarEscapeStatus "root" $}}
{{.sidebarContent | SafeHTML}}
{{.WikiSidebarHTML}}
</div>
{{end}}
<div class="tw-clear-both"></div>
{{if .footerPresent}}
{{if .WikiFooterHTML}}
<div class="render-content markup wiki-content-footer">
{{if and .CanWriteWiki (not .Repository.IsMirror)}}
<a class="tw-float-right muted" href="{{.RepoLink}}/wiki/_Footer?action=_edit" aria-label="{{ctx.Locale.Tr "repo.wiki.edit_page_button"}}">{{svg "octicon-pencil"}}</a>
{{end}}
{{template "repo/unicode_escape_prompt" dict "footerEscapeStatus" .sidebarEscapeStatus "root" $}}
{{.footerContent | SafeHTML}}
{{.WikiFooterHTML}}
</div>
{{end}}
</div>

View File

@ -7,6 +7,7 @@ import (
"bytes"
"fmt"
"io"
"maps"
"mime/multipart"
"net/http"
"net/http/httptest"
@ -19,292 +20,278 @@ import (
"code.gitea.io/gitea/models/unittest"
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/test"
"code.gitea.io/gitea/modules/translation"
"code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/tests"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestCreateFile(t *testing.T) {
func TestEditor(t *testing.T) {
onGiteaRun(t, func(t *testing.T, u *url.URL) {
session := loginUser(t, "user2")
testCreateFile(t, session, "user2", "repo1", "master", "test.txt", "Content")
sessionUser2 := loginUser(t, "user2")
t.Run("EditFileNotAllowed", testEditFileNotAllowed)
t.Run("DiffPreview", testEditorDiffPreview)
t.Run("CreateFile", testEditorCreateFile)
t.Run("EditFile", func(t *testing.T) {
testEditFile(t, sessionUser2, "user2", "repo1", "master", "README.md", "Hello, World (direct)\n")
testEditFileToNewBranch(t, sessionUser2, "user2", "repo1", "master", "feature/test", "README.md", "Hello, World (commit-to-new-branch)\n")
})
t.Run("PatchFile", testEditorPatchFile)
t.Run("DeleteFile", func(t *testing.T) {
viewLink := "/user2/repo1/src/branch/branch2/README.md"
sessionUser2.MakeRequest(t, NewRequest(t, "GET", viewLink), http.StatusOK)
testEditorActionPostRequest(t, sessionUser2, "/user2/repo1/_delete/branch2/README.md", map[string]string{"commit_choice": "direct"})
sessionUser2.MakeRequest(t, NewRequest(t, "GET", viewLink), http.StatusNotFound)
})
t.Run("ForkToEditFile", func(t *testing.T) {
testForkToEditFile(t, loginUser(t, "user4"), "user4", "user2", "repo1", "master", "README.md")
})
t.Run("WebGitCommitEmail", testEditorWebGitCommitEmail)
t.Run("ProtectedBranch", testEditorProtectedBranch)
})
}
func testEditorCreateFile(t *testing.T) {
session := loginUser(t, "user2")
testCreateFile(t, session, "user2", "repo1", "master", "test.txt", "Content")
testEditorActionPostRequestError(t, session, "/user2/repo1/_new/master/", map[string]string{
"tree_path": "test.txt",
"commit_choice": "direct",
"new_branch_name": "master",
}, `A file named "test.txt" already exists in this repository.`)
testEditorActionPostRequestError(t, session, "/user2/repo1/_new/master/", map[string]string{
"tree_path": "test.txt",
"commit_choice": "commit-to-new-branch",
"new_branch_name": "master",
}, `Branch "master" already exists in this repository.`)
}
func testCreateFile(t *testing.T, session *TestSession, user, repo, branch, filePath, content string) {
// Request editor page
newURL := fmt.Sprintf("/%s/%s/_new/%s/", user, repo, branch)
req := NewRequest(t, "GET", newURL)
resp := session.MakeRequest(t, req, http.StatusOK)
doc := NewHTMLParser(t, resp.Body)
lastCommit := doc.GetInputValueByName("last_commit")
assert.NotEmpty(t, lastCommit)
// Save new file to master branch
req = NewRequestWithValues(t, "POST", newURL, map[string]string{
"_csrf": doc.GetCSRF(),
"last_commit": lastCommit,
testEditorActionEdit(t, session, user, repo, "_new", branch, "", map[string]string{
"tree_path": filePath,
"content": content,
"commit_choice": "direct",
})
resp = session.MakeRequest(t, req, http.StatusOK)
assert.NotEmpty(t, test.RedirectURL(resp))
}
func TestCreateFileOnProtectedBranch(t *testing.T) {
onGiteaRun(t, func(t *testing.T, u *url.URL) {
session := loginUser(t, "user2")
func testEditorProtectedBranch(t *testing.T) {
session := loginUser(t, "user2")
// Change the "master" branch to "protected"
req := NewRequestWithValues(t, "POST", "/user2/repo1/settings/branches/edit", map[string]string{
"_csrf": GetUserCSRFToken(t, session),
"rule_name": "master",
"enable_push": "true",
})
session.MakeRequest(t, req, http.StatusSeeOther)
flashMsg := session.GetCookieFlashMessage()
assert.Equal(t, `Branch protection for rule "master" has been updated.`, flashMsg.SuccessMsg)
csrf := GetUserCSRFToken(t, session)
// Change master branch to protected
req := NewRequestWithValues(t, "POST", "/user2/repo1/settings/branches/edit", map[string]string{
"_csrf": csrf,
"rule_name": "master",
"enable_push": "true",
})
session.MakeRequest(t, req, http.StatusSeeOther)
// Check if master branch has been locked successfully
flashMsg := session.GetCookieFlashMessage()
assert.Equal(t, `Branch protection for rule "master" has been updated.`, flashMsg.SuccessMsg)
// Try to commit a file to the "master" branch and it should fail
resp := testEditorActionPostRequest(t, session, "/user2/repo1/_new/master/", map[string]string{"tree_path": "test-protected-branch.txt", "commit_choice": "direct"})
assert.Equal(t, http.StatusBadRequest, resp.Code)
assert.Equal(t, `Cannot commit to protected branch "master".`, test.ParseJSONError(resp.Body.Bytes()).ErrorMessage)
}
// Request editor page
req = NewRequest(t, "GET", "/user2/repo1/_new/master/")
func testEditorActionPostRequest(t *testing.T, session *TestSession, requestPath string, params map[string]string) *httptest.ResponseRecorder {
req := NewRequest(t, "GET", requestPath)
resp := session.MakeRequest(t, req, http.StatusOK)
htmlDoc := NewHTMLParser(t, resp.Body)
form := map[string]string{
"_csrf": htmlDoc.GetCSRF(),
"last_commit": htmlDoc.GetInputValueByName("last_commit"),
}
maps.Copy(form, params)
req = NewRequestWithValues(t, "POST", requestPath, form)
return session.MakeRequest(t, req, NoExpectedStatus)
}
func testEditorActionPostRequestError(t *testing.T, session *TestSession, requestPath string, params map[string]string, errorMessage string) {
resp := testEditorActionPostRequest(t, session, requestPath, params)
assert.Equal(t, http.StatusBadRequest, resp.Code)
assert.Equal(t, errorMessage, test.ParseJSONError(resp.Body.Bytes()).ErrorMessage)
}
func testEditorActionEdit(t *testing.T, session *TestSession, user, repo, editorAction, branch, filePath string, params map[string]string) *httptest.ResponseRecorder {
params["tree_path"] = util.IfZero(params["tree_path"], filePath)
newBranchName := util.Iif(params["commit_choice"] == "direct", branch, params["new_branch_name"])
resp := testEditorActionPostRequest(t, session, fmt.Sprintf("/%s/%s/%s/%s/%s", user, repo, editorAction, branch, filePath), params)
assert.Equal(t, http.StatusOK, resp.Code)
assert.NotEmpty(t, test.RedirectURL(resp))
req := NewRequest(t, "GET", path.Join(user, repo, "raw/branch", newBranchName, params["tree_path"]))
resp = session.MakeRequest(t, req, http.StatusOK)
assert.Equal(t, params["content"], resp.Body.String())
return resp
}
func testEditFile(t *testing.T, session *TestSession, user, repo, branch, filePath, newContent string) {
testEditorActionEdit(t, session, user, repo, "_edit", branch, filePath, map[string]string{
"content": newContent,
"commit_choice": "direct",
})
}
func testEditFileToNewBranch(t *testing.T, session *TestSession, user, repo, branch, targetBranch, filePath, newContent string) {
testEditorActionEdit(t, session, user, repo, "_edit", branch, filePath, map[string]string{
"content": newContent,
"commit_choice": "commit-to-new-branch",
"new_branch_name": targetBranch,
})
}
func testEditorDiffPreview(t *testing.T) {
session := loginUser(t, "user2")
req := NewRequestWithValues(t, "POST", "/user2/repo1/_preview/master/README.md", map[string]string{
"_csrf": GetUserCSRFToken(t, session),
"content": "Hello, World (Edited)\n",
})
resp := session.MakeRequest(t, req, http.StatusOK)
assert.Contains(t, resp.Body.String(), `<span class="added-code">Hello, World (Edited)</span>`)
}
func testEditorPatchFile(t *testing.T) {
session := loginUser(t, "user2")
pathContentCommon := `diff --git a/patch-file-1.txt b/patch-file-1.txt
new file mode 100644
index 0000000000..aaaaaaaaaa
--- /dev/null
+++ b/patch-file-1.txt
@@ -0,0 +1 @@
+`
testEditorActionPostRequest(t, session, "/user2/repo1/_diffpatch/master/", map[string]string{
"content": pathContentCommon + "patched content\n",
"commit_choice": "commit-to-new-branch",
"new_branch_name": "patched-branch",
})
resp := MakeRequest(t, NewRequest(t, "GET", "/user2/repo1/raw/branch/patched-branch/patch-file-1.txt"), http.StatusOK)
assert.Equal(t, "patched content\n", resp.Body.String())
// patch again, it should fail
resp = testEditorActionPostRequest(t, session, "/user2/repo1/_diffpatch/patched-branch/", map[string]string{
"content": pathContentCommon + "another patched content\n",
"commit_choice": "commit-to-new-branch",
"new_branch_name": "patched-branch-1",
})
assert.Equal(t, "Unable to apply patch", test.ParseJSONError(resp.Body.Bytes()).ErrorMessage)
}
func testEditorWebGitCommitEmail(t *testing.T) {
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
require.True(t, user.KeepEmailPrivate)
repo1 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
gitRepo, _ := git.OpenRepository(git.DefaultContext, repo1.RepoPath())
defer gitRepo.Close()
getLastCommit := func(t *testing.T) *git.Commit {
c, err := gitRepo.GetBranchCommit("master")
require.NoError(t, err)
return c
}
session := loginUser(t, user.Name)
makeReq := func(t *testing.T, link string, params map[string]string, expectedUserName, expectedEmail string) *httptest.ResponseRecorder {
lastCommit := getLastCommit(t)
params["_csrf"] = GetUserCSRFToken(t, session)
params["last_commit"] = lastCommit.ID.String()
params["commit_choice"] = "direct"
req := NewRequestWithValues(t, "POST", link, params)
resp := session.MakeRequest(t, req, NoExpectedStatus)
newCommit := getLastCommit(t)
if expectedUserName == "" {
require.Equal(t, lastCommit.ID.String(), newCommit.ID.String())
respErr := test.ParseJSONError(resp.Body.Bytes())
assert.Equal(t, translation.NewLocale("en-US").TrString("repo.editor.invalid_commit_email"), respErr.ErrorMessage)
} else {
require.NotEqual(t, lastCommit.ID.String(), newCommit.ID.String())
assert.Equal(t, expectedUserName, newCommit.Author.Name)
assert.Equal(t, expectedEmail, newCommit.Author.Email)
assert.Equal(t, expectedUserName, newCommit.Committer.Name)
assert.Equal(t, expectedEmail, newCommit.Committer.Email)
}
return resp
}
uploadFile := func(t *testing.T, name, content string) string {
body := &bytes.Buffer{}
uploadForm := multipart.NewWriter(body)
file, _ := uploadForm.CreateFormFile("file", name)
_, _ = io.Copy(file, strings.NewReader(content))
_ = uploadForm.WriteField("_csrf", GetUserCSRFToken(t, session))
_ = uploadForm.Close()
req := NewRequestWithBody(t, "POST", "/user2/repo1/upload-file", body)
req.Header.Add("Content-Type", uploadForm.FormDataContentType())
resp := session.MakeRequest(t, req, http.StatusOK)
doc := NewHTMLParser(t, resp.Body)
lastCommit := doc.GetInputValueByName("last_commit")
assert.NotEmpty(t, lastCommit)
respMap := map[string]string{}
DecodeJSON(t, resp, &respMap)
return respMap["uuid"]
}
// Save new file to master branch
req = NewRequestWithValues(t, "POST", "/user2/repo1/_new/master/", map[string]string{
"_csrf": doc.GetCSRF(),
"last_commit": lastCommit,
"tree_path": "test.txt",
"content": "Content",
"commit_choice": "direct",
})
resp = session.MakeRequest(t, req, http.StatusBadRequest)
respErr := test.ParseJSONError(resp.Body.Bytes())
assert.Equal(t, `Cannot commit to protected branch "master".`, respErr.ErrorMessage)
// remove the protected branch
csrf = GetUserCSRFToken(t, session)
// Change master branch to protected
req = NewRequestWithValues(t, "POST", "/user2/repo1/settings/branches/1/delete", map[string]string{
"_csrf": csrf,
})
resp = session.MakeRequest(t, req, http.StatusOK)
res := make(map[string]string)
assert.NoError(t, json.NewDecoder(resp.Body).Decode(&res))
assert.Equal(t, "/user2/repo1/settings/branches", res["redirect"])
// Check if master branch has been locked successfully
flashMsg = session.GetCookieFlashMessage()
assert.Equal(t, `Removing branch protection rule "1" failed.`, flashMsg.ErrorMsg)
t.Run("EmailInactive", func(t *testing.T) {
defer tests.PrintCurrentTest(t)()
email := unittest.AssertExistsAndLoadBean(t, &user_model.EmailAddress{ID: 35, UID: user.ID})
require.False(t, email.IsActivated)
makeReq(t, "/user2/repo1/_edit/master/README.md", map[string]string{
"tree_path": "README.md",
"content": "test content",
"commit_email": email.Email,
}, "", "")
})
}
func testEditFile(t *testing.T, session *TestSession, user, repo, branch, filePath, newContent string) *httptest.ResponseRecorder {
// Get to the 'edit this file' page
req := NewRequest(t, "GET", path.Join(user, repo, "_edit", branch, filePath))
resp := session.MakeRequest(t, req, http.StatusOK)
htmlDoc := NewHTMLParser(t, resp.Body)
lastCommit := htmlDoc.GetInputValueByName("last_commit")
assert.NotEmpty(t, lastCommit)
// Submit the edits
req = NewRequestWithValues(t, "POST", path.Join(user, repo, "_edit", branch, filePath),
map[string]string{
"_csrf": htmlDoc.GetCSRF(),
"last_commit": lastCommit,
"tree_path": filePath,
"content": newContent,
"commit_choice": "direct",
},
)
resp = session.MakeRequest(t, req, http.StatusOK)
assert.NotEmpty(t, test.RedirectURL(resp))
// Verify the change
req = NewRequest(t, "GET", path.Join(user, repo, "raw/branch", branch, filePath))
resp = session.MakeRequest(t, req, http.StatusOK)
assert.Equal(t, newContent, resp.Body.String())
return resp
}
func testEditFileToNewBranch(t *testing.T, session *TestSession, user, repo, branch, targetBranch, filePath, newContent string) *httptest.ResponseRecorder {
// Get to the 'edit this file' page
req := NewRequest(t, "GET", path.Join(user, repo, "_edit", branch, filePath))
resp := session.MakeRequest(t, req, http.StatusOK)
htmlDoc := NewHTMLParser(t, resp.Body)
lastCommit := htmlDoc.GetInputValueByName("last_commit")
assert.NotEmpty(t, lastCommit)
// Submit the edits
req = NewRequestWithValues(t, "POST", path.Join(user, repo, "_edit", branch, filePath),
map[string]string{
"_csrf": htmlDoc.GetCSRF(),
"last_commit": lastCommit,
"tree_path": filePath,
"content": newContent,
"commit_choice": "commit-to-new-branch",
"new_branch_name": targetBranch,
},
)
resp = session.MakeRequest(t, req, http.StatusOK)
assert.NotEmpty(t, test.RedirectURL(resp))
// Verify the change
req = NewRequest(t, "GET", path.Join(user, repo, "raw/branch", targetBranch, filePath))
resp = session.MakeRequest(t, req, http.StatusOK)
assert.Equal(t, newContent, resp.Body.String())
return resp
}
func TestEditFile(t *testing.T) {
onGiteaRun(t, func(t *testing.T, u *url.URL) {
session := loginUser(t, "user2")
testEditFile(t, session, "user2", "repo1", "master", "README.md", "Hello, World (Edited)\n")
t.Run("EmailInvalid", func(t *testing.T) {
defer tests.PrintCurrentTest(t)()
email := unittest.AssertExistsAndLoadBean(t, &user_model.EmailAddress{ID: 1, IsActivated: true})
require.NotEqual(t, email.UID, user.ID)
makeReq(t, "/user2/repo1/_edit/master/README.md", map[string]string{
"tree_path": "README.md",
"content": "test content",
"commit_email": email.Email,
}, "", "")
})
}
func TestEditFileToNewBranch(t *testing.T) {
onGiteaRun(t, func(t *testing.T, u *url.URL) {
session := loginUser(t, "user2")
testEditFileToNewBranch(t, session, "user2", "repo1", "master", "feature/test", "README.md", "Hello, World (Edited)\n")
})
}
func TestWebGitCommitEmail(t *testing.T) {
onGiteaRun(t, func(t *testing.T, _ *url.URL) {
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
require.True(t, user.KeepEmailPrivate)
repo1 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
gitRepo, _ := git.OpenRepository(git.DefaultContext, repo1.RepoPath())
defer gitRepo.Close()
getLastCommit := func(t *testing.T) *git.Commit {
c, err := gitRepo.GetBranchCommit("master")
require.NoError(t, err)
return c
}
session := loginUser(t, user.Name)
makeReq := func(t *testing.T, link string, params map[string]string, expectedUserName, expectedEmail string) *httptest.ResponseRecorder {
lastCommit := getLastCommit(t)
params["_csrf"] = GetUserCSRFToken(t, session)
params["last_commit"] = lastCommit.ID.String()
params["commit_choice"] = "direct"
req := NewRequestWithValues(t, "POST", link, params)
resp := session.MakeRequest(t, req, NoExpectedStatus)
newCommit := getLastCommit(t)
if expectedUserName == "" {
require.Equal(t, lastCommit.ID.String(), newCommit.ID.String())
respErr := test.ParseJSONError(resp.Body.Bytes())
assert.Equal(t, translation.NewLocale("en-US").TrString("repo.editor.invalid_commit_email"), respErr.ErrorMessage)
} else {
require.NotEqual(t, lastCommit.ID.String(), newCommit.ID.String())
assert.Equal(t, expectedUserName, newCommit.Author.Name)
assert.Equal(t, expectedEmail, newCommit.Author.Email)
assert.Equal(t, expectedUserName, newCommit.Committer.Name)
assert.Equal(t, expectedEmail, newCommit.Committer.Email)
}
return resp
}
uploadFile := func(t *testing.T, name, content string) string {
body := &bytes.Buffer{}
uploadForm := multipart.NewWriter(body)
file, _ := uploadForm.CreateFormFile("file", name)
_, _ = io.Copy(file, strings.NewReader(content))
_ = uploadForm.WriteField("_csrf", GetUserCSRFToken(t, session))
_ = uploadForm.Close()
req := NewRequestWithBody(t, "POST", "/user2/repo1/upload-file", body)
req.Header.Add("Content-Type", uploadForm.FormDataContentType())
resp := session.MakeRequest(t, req, http.StatusOK)
respMap := map[string]string{}
DecodeJSON(t, resp, &respMap)
return respMap["uuid"]
}
t.Run("EmailInactive", func(t *testing.T) {
testWebGit := func(t *testing.T, linkForKeepPrivate string, paramsForKeepPrivate map[string]string, linkForChosenEmail string, paramsForChosenEmail map[string]string) (resp1, resp2 *httptest.ResponseRecorder) {
t.Run("DefaultEmailKeepPrivate", func(t *testing.T) {
defer tests.PrintCurrentTest(t)()
email := unittest.AssertExistsAndLoadBean(t, &user_model.EmailAddress{ID: 35, UID: user.ID})
require.False(t, email.IsActivated)
makeReq(t, "/user2/repo1/_edit/master/README.md", map[string]string{
"tree_path": "README.md",
"content": "test content",
"commit_email": email.Email,
}, "", "")
paramsForKeepPrivate["commit_email"] = ""
resp1 = makeReq(t, linkForKeepPrivate, paramsForKeepPrivate, "User Two", "user2@noreply.example.org")
})
t.Run("EmailInvalid", func(t *testing.T) {
t.Run("ChooseEmail", func(t *testing.T) {
defer tests.PrintCurrentTest(t)()
email := unittest.AssertExistsAndLoadBean(t, &user_model.EmailAddress{ID: 1, IsActivated: true})
require.NotEqual(t, email.UID, user.ID)
makeReq(t, "/user2/repo1/_edit/master/README.md", map[string]string{
"tree_path": "README.md",
"content": "test content",
"commit_email": email.Email,
}, "", "")
paramsForChosenEmail["commit_email"] = "user2@example.com"
resp2 = makeReq(t, linkForChosenEmail, paramsForChosenEmail, "User Two", "user2@example.com")
})
return resp1, resp2
}
testWebGit := func(t *testing.T, linkForKeepPrivate string, paramsForKeepPrivate map[string]string, linkForChosenEmail string, paramsForChosenEmail map[string]string) (resp1, resp2 *httptest.ResponseRecorder) {
t.Run("DefaultEmailKeepPrivate", func(t *testing.T) {
defer tests.PrintCurrentTest(t)()
paramsForKeepPrivate["commit_email"] = ""
resp1 = makeReq(t, linkForKeepPrivate, paramsForKeepPrivate, "User Two", "user2@noreply.example.org")
})
t.Run("ChooseEmail", func(t *testing.T) {
defer tests.PrintCurrentTest(t)()
paramsForChosenEmail["commit_email"] = "user2@example.com"
resp2 = makeReq(t, linkForChosenEmail, paramsForChosenEmail, "User Two", "user2@example.com")
})
return resp1, resp2
}
t.Run("Edit", func(t *testing.T) {
testWebGit(t,
"/user2/repo1/_edit/master/README.md", map[string]string{"tree_path": "README.md", "content": "for keep private"},
"/user2/repo1/_edit/master/README.md", map[string]string{"tree_path": "README.md", "content": "for chosen email"},
)
})
t.Run("Edit", func(t *testing.T) {
testWebGit(t,
"/user2/repo1/_edit/master/README.md", map[string]string{"tree_path": "README.md", "content": "for keep private"},
"/user2/repo1/_edit/master/README.md", map[string]string{"tree_path": "README.md", "content": "for chosen email"},
)
})
t.Run("UploadDelete", func(t *testing.T) {
file1UUID := uploadFile(t, "file1", "File 1")
file2UUID := uploadFile(t, "file2", "File 2")
testWebGit(t,
"/user2/repo1/_upload/master", map[string]string{"files": file1UUID},
"/user2/repo1/_upload/master", map[string]string{"files": file2UUID},
)
testWebGit(t,
"/user2/repo1/_delete/master/file1", map[string]string{},
"/user2/repo1/_delete/master/file2", map[string]string{},
)
})
t.Run("UploadDelete", func(t *testing.T) {
file1UUID := uploadFile(t, "file1", "File 1")
file2UUID := uploadFile(t, "file2", "File 2")
testWebGit(t,
"/user2/repo1/_upload/master", map[string]string{"files": file1UUID},
"/user2/repo1/_upload/master", map[string]string{"files": file2UUID},
)
testWebGit(t,
"/user2/repo1/_delete/master/file1", map[string]string{},
"/user2/repo1/_delete/master/file2", map[string]string{},
)
})
t.Run("ApplyPatchCherryPick", func(t *testing.T) {
testWebGit(t,
"/user2/repo1/_diffpatch/master", map[string]string{
"tree_path": "__dummy__",
"content": `diff --git a/patch-file-1.txt b/patch-file-1.txt
t.Run("ApplyPatchCherryPick", func(t *testing.T) {
testWebGit(t,
"/user2/repo1/_diffpatch/master", map[string]string{
"tree_path": "__dummy__",
"content": `diff --git a/patch-file-1.txt b/patch-file-1.txt
new file mode 100644
index 0000000000..aaaaaaaaaa
--- /dev/null
@ -312,10 +299,10 @@ index 0000000000..aaaaaaaaaa
@@ -0,0 +1 @@
+File 1
`,
},
"/user2/repo1/_diffpatch/master", map[string]string{
"tree_path": "__dummy__",
"content": `diff --git a/patch-file-2.txt b/patch-file-2.txt
},
"/user2/repo1/_diffpatch/master", map[string]string{
"tree_path": "__dummy__",
"content": `diff --git a/patch-file-2.txt b/patch-file-2.txt
new file mode 100644
index 0000000000..bbbbbbbbbb
--- /dev/null
@ -323,20 +310,146 @@ index 0000000000..bbbbbbbbbb
@@ -0,0 +1 @@
+File 2
`,
},
)
},
)
commit1, err := gitRepo.GetCommitByPath("patch-file-1.txt")
require.NoError(t, err)
commit2, err := gitRepo.GetCommitByPath("patch-file-2.txt")
require.NoError(t, err)
resp1, _ := testWebGit(t,
"/user2/repo1/_cherrypick/"+commit1.ID.String()+"/master", map[string]string{"revert": "true"},
"/user2/repo1/_cherrypick/"+commit2.ID.String()+"/master", map[string]string{"revert": "true"},
)
commit1, err := gitRepo.GetCommitByPath("patch-file-1.txt")
require.NoError(t, err)
commit2, err := gitRepo.GetCommitByPath("patch-file-2.txt")
require.NoError(t, err)
resp1, _ := testWebGit(t,
"/user2/repo1/_cherrypick/"+commit1.ID.String()+"/master", map[string]string{"revert": "true"},
"/user2/repo1/_cherrypick/"+commit2.ID.String()+"/master", map[string]string{"revert": "true"},
)
// By the way, test the "cherrypick" page: a successful revert redirects to the main branch
assert.Equal(t, "/user2/repo1/src/branch/master", test.RedirectURL(resp1))
})
// By the way, test the "cherrypick" page: a successful revert redirects to the main branch
assert.Equal(t, "/user2/repo1/src/branch/master", test.RedirectURL(resp1))
})
}
func testForkToEditFile(t *testing.T, session *TestSession, user, owner, repo, branch, filePath string) {
forkToEdit := func(t *testing.T, session *TestSession, owner, repo, operation, branch, filePath string) {
// visit the base repo, see the "Add File" button
req := NewRequest(t, "GET", path.Join(owner, repo))
resp := session.MakeRequest(t, req, http.StatusOK)
htmlDoc := NewHTMLParser(t, resp.Body)
AssertHTMLElement(t, htmlDoc, ".repo-add-file", 1)
// attempt to edit a file, see the guideline page
req = NewRequest(t, "GET", path.Join(owner, repo, operation, branch, filePath))
resp = session.MakeRequest(t, req, http.StatusOK)
assert.Contains(t, resp.Body.String(), "Fork Repository to Propose Changes")
// fork the repository
req = NewRequestWithValues(t, "POST", path.Join(owner, repo, "_fork", branch), map[string]string{"_csrf": GetUserCSRFToken(t, session)})
resp = session.MakeRequest(t, req, http.StatusOK)
assert.JSONEq(t, `{"redirect":""}`, resp.Body.String())
}
t.Run("ForkButArchived", func(t *testing.T) {
// Fork repository because we can't edit it
forkToEdit(t, session, owner, repo, "_edit", branch, filePath)
// Archive the repository
req := NewRequestWithValues(t, "POST", path.Join(user, repo, "settings"),
map[string]string{
"_csrf": GetUserCSRFToken(t, session),
"repo_name": repo,
"action": "archive",
},
)
session.MakeRequest(t, req, http.StatusSeeOther)
// Check editing archived repository is disabled
req = NewRequest(t, "GET", path.Join(owner, repo, "_edit", branch, filePath)).SetHeader("Accept", "text/html")
resp := session.MakeRequest(t, req, http.StatusNotFound)
assert.Contains(t, resp.Body.String(), "You have forked this repository but your fork is not editable.")
// Unfork the repository
req = NewRequestWithValues(t, "POST", path.Join(user, repo, "settings"),
map[string]string{
"_csrf": GetUserCSRFToken(t, session),
"repo_name": repo,
"action": "convert_fork",
},
)
session.MakeRequest(t, req, http.StatusSeeOther)
})
// Fork repository again, and check the existence of the forked repo with unique name
forkToEdit(t, session, owner, repo, "_edit", branch, filePath)
session.MakeRequest(t, NewRequestf(t, "GET", "/%s/%s-1", user, repo), http.StatusOK)
t.Run("CheckBaseRepoForm", func(t *testing.T) {
// the base repo's edit form should have the correct action and upload links (pointing to the forked repo)
req := NewRequest(t, "GET", path.Join(owner, repo, "_upload", branch, filePath)+"?foo=bar")
resp := session.MakeRequest(t, req, http.StatusOK)
htmlDoc := NewHTMLParser(t, resp.Body)
uploadForm := htmlDoc.doc.Find(".form-fetch-action")
formAction := uploadForm.AttrOr("action", "")
assert.Equal(t, fmt.Sprintf("/%s/%s-1/_upload/%s/%s?from_base_branch=%s&foo=bar", user, repo, branch, filePath, branch), formAction)
uploadLink := uploadForm.Find(".dropzone").AttrOr("data-link-url", "")
assert.Equal(t, fmt.Sprintf("/%s/%s-1/upload-file", user, repo), uploadLink)
newBranchName := uploadForm.Find("input[name=new_branch_name]").AttrOr("value", "")
assert.Equal(t, user+"-patch-1", newBranchName)
commitChoice := uploadForm.Find("input[name=commit_choice][checked]").AttrOr("value", "")
assert.Equal(t, "commit-to-new-branch", commitChoice)
lastCommit := uploadForm.Find("input[name=last_commit]").AttrOr("value", "")
assert.NotEmpty(t, lastCommit)
})
t.Run("ViewBaseEditFormAndCommitToFork", func(t *testing.T) {
req := NewRequest(t, "GET", path.Join(owner, repo, "_edit", branch, filePath))
resp := session.MakeRequest(t, req, http.StatusOK)
htmlDoc := NewHTMLParser(t, resp.Body)
editRequestForm := map[string]string{
"_csrf": GetUserCSRFToken(t, session),
"last_commit": htmlDoc.GetInputValueByName("last_commit"),
"tree_path": filePath,
"content": "new content in fork",
"commit_choice": "commit-to-new-branch",
}
// change a file in the forked repo with existing branch name (should fail)
editRequestForm["new_branch_name"] = "master"
req = NewRequestWithValues(t, "POST", fmt.Sprintf("/%s/%s-1/_edit/%s/%s?from_base_branch=%s", user, repo, branch, filePath, branch), editRequestForm)
resp = session.MakeRequest(t, req, http.StatusBadRequest)
respJSON := test.ParseJSONError(resp.Body.Bytes())
assert.Equal(t, `Branch "master" already exists in your fork, please choose a new branch name.`, respJSON.ErrorMessage)
// change a file in the forked repo (should succeed)
newBranchName := htmlDoc.GetInputValueByName("new_branch_name")
editRequestForm["new_branch_name"] = newBranchName
req = NewRequestWithValues(t, "POST", fmt.Sprintf("/%s/%s-1/_edit/%s/%s?from_base_branch=%s", user, repo, branch, filePath, branch), editRequestForm)
resp = session.MakeRequest(t, req, http.StatusOK)
assert.Equal(t, fmt.Sprintf("/%s/%s/compare/%s...%s/%s-1:%s", owner, repo, branch, user, repo, newBranchName), test.RedirectURL(resp))
// check the file in the fork's branch is changed
req = NewRequest(t, "GET", fmt.Sprintf("/%s/%s-1/src/branch/%s/%s", user, repo, newBranchName, filePath))
resp = session.MakeRequest(t, req, http.StatusOK)
assert.Contains(t, resp.Body.String(), "new content in fork")
})
}
func testEditFileNotAllowed(t *testing.T) {
sessionUser1 := loginUser(t, "user1") // admin, all access
sessionUser4 := loginUser(t, "user4")
// "_cherrypick" has a different route pattern, so skip its test
operations := []string{"_new", "_edit", "_delete", "_upload", "_diffpatch"}
for _, operation := range operations {
t.Run(operation, func(t *testing.T) {
// Branch does not exist
targetLink := path.Join("user2", "repo1", operation, "missing", "README.md")
sessionUser1.MakeRequest(t, NewRequest(t, "GET", targetLink), http.StatusNotFound)
// Private repository
targetLink = path.Join("user2", "repo2", operation, "master", "Home.md")
sessionUser1.MakeRequest(t, NewRequest(t, "GET", targetLink), http.StatusOK)
sessionUser4.MakeRequest(t, NewRequest(t, "GET", targetLink), http.StatusNotFound)
// Empty repository
targetLink = path.Join("org41", "repo61", operation, "master", "README.md")
sessionUser1.MakeRequest(t, NewRequest(t, "GET", targetLink), http.StatusNotFound)
})
}
}

View File

@ -42,7 +42,7 @@ func (doc *HTMLDoc) GetCSRF() string {
return doc.GetInputValueByName("_csrf")
}
// AssertHTMLElement check if element by selector exists or does not exist depending on checkExists
// AssertHTMLElement check if the element by selector exists or does not exist depending on checkExists
func AssertHTMLElement[T int | bool](t testing.TB, doc *HTMLDoc, selector string, checkExists T) {
sel := doc.doc.Find(selector)
switch v := any(checkExists).(type) {

View File

@ -129,7 +129,7 @@ func TestPullCreate_EmptyChangesWithDifferentCommits(t *testing.T) {
session := loginUser(t, "user1")
testRepoFork(t, session, "user2", "repo1", "user1", "repo1", "")
testEditFileToNewBranch(t, session, "user1", "repo1", "master", "status1", "README.md", "status1")
testEditFileToNewBranch(t, session, "user1", "repo1", "status1", "status1", "README.md", "# repo1\n\nDescription for repo1")
testEditFile(t, session, "user1", "repo1", "status1", "README.md", "# repo1\n\nDescription for repo1")
url := path.Join("user1", "repo1", "compare", "master...status1")
req := NewRequestWithValues(t, "POST", url,

View File

@ -134,7 +134,9 @@
margin-bottom: 16px;
}
/* override p:last-child from base.css */
/* override p:last-child from base.css.
Fomantic assumes that <p>/<hX> elements only have margins between elements, but not for the first's top or last's bottom.
In markup content, we always use bottom margin for all elements */
.markup p:last-child {
margin-bottom: 16px;
}

View File

@ -1834,6 +1834,7 @@ tbody.commit-list {
border-radius: 0;
display: flex;
flex-direction: column;
gap: 0.5em;
}
/* fomantic's last-child selector does not work with hidden last child */

View File

@ -39,10 +39,6 @@
min-width: 150px;
}
.repository.wiki .wiki-content-sidebar .ui.message.unicode-escape-prompt p {
display: none;
}
.repository.wiki .wiki-content-footer {
margin-top: 1em;
}

View File

@ -1,7 +1,7 @@
<script lang="ts" setup>
// TODO: Switch to upstream after https://github.com/razorness/vue3-calendar-heatmap/pull/34 is merged
import {CalendarHeatmap} from '@silverwind/vue3-calendar-heatmap';
import {onMounted, ref} from 'vue';
import {onMounted, shallowRef} from 'vue';
import type {Value as HeatmapValue, Locale as HeatmapLocale} from '@silverwind/vue3-calendar-heatmap';
defineProps<{
@ -24,7 +24,7 @@ const colorRange = [
'var(--color-primary-dark-4)',
];
const endDate = ref(new Date());
const endDate = shallowRef(new Date());
onMounted(() => {
// work around issue with first legend color being rendered twice and legend cut off

View File

@ -2,16 +2,16 @@
import {SvgIcon} from '../svg.ts';
import {GET} from '../modules/fetch.ts';
import {getIssueColor, getIssueIcon} from '../features/issue.ts';
import {computed, onMounted, ref} from 'vue';
import {computed, onMounted, shallowRef, useTemplateRef} from 'vue';
import type {IssuePathInfo} from '../types.ts';
const {appSubUrl, i18n} = window.config;
const loading = ref(false);
const issue = ref(null);
const renderedLabels = ref('');
const loading = shallowRef(false);
const issue = shallowRef(null);
const renderedLabels = shallowRef('');
const i18nErrorOccurred = i18n.error_occurred;
const i18nErrorMessage = ref(null);
const i18nErrorMessage = shallowRef(null);
const createdAt = computed(() => new Date(issue.value.created_at).toLocaleDateString(undefined, {year: 'numeric', month: 'short', day: 'numeric'}));
const body = computed(() => {
@ -22,7 +22,7 @@ const body = computed(() => {
return body;
});
const root = ref<HTMLElement | null>(null);
const root = useTemplateRef('root');
onMounted(() => {
root.value.addEventListener('ce-load-context-popup', (e: CustomEventInit<IssuePathInfo>) => {

View File

@ -1,6 +1,6 @@
<script lang="ts" setup>
import {SvgIcon, type SvgName} from '../svg.ts';
import {ref} from 'vue';
import {shallowRef} from 'vue';
import {type DiffStatus, type DiffTreeEntry, diffTreeStore} from '../modules/diff-file.ts';
const props = defineProps<{
@ -8,7 +8,7 @@ const props = defineProps<{
}>();
const store = diffTreeStore();
const collapsed = ref(props.item.IsViewed);
const collapsed = shallowRef(props.item.IsViewed);
function getIconForDiffStatus(pType: DiffStatus) {
const diffTypes: Record<DiffStatus, { name: SvgName, classes: Array<string> }> = {

View File

@ -1,19 +1,19 @@
<script lang="ts" setup>
import {computed, onMounted, onUnmounted, ref, watch} from 'vue';
import {computed, onMounted, onUnmounted, shallowRef, watch} from 'vue';
import {SvgIcon} from '../svg.ts';
import {toggleElem} from '../utils/dom.ts';
const {csrfToken, pageData} = window.config;
const mergeForm = ref(pageData.pullRequestMergeForm);
const mergeForm = pageData.pullRequestMergeForm;
const mergeTitleFieldValue = ref('');
const mergeMessageFieldValue = ref('');
const deleteBranchAfterMerge = ref(false);
const autoMergeWhenSucceed = ref(false);
const mergeTitleFieldValue = shallowRef('');
const mergeMessageFieldValue = shallowRef('');
const deleteBranchAfterMerge = shallowRef(false);
const autoMergeWhenSucceed = shallowRef(false);
const mergeStyle = ref('');
const mergeStyleDetail = ref({
const mergeStyle = shallowRef('');
const mergeStyleDetail = shallowRef({
hideMergeMessageTexts: false,
textDoMerge: '',
mergeTitleFieldText: '',
@ -21,33 +21,33 @@ const mergeStyleDetail = ref({
hideAutoMerge: false,
});
const mergeStyleAllowedCount = ref(0);
const mergeStyleAllowedCount = shallowRef(0);
const showMergeStyleMenu = ref(false);
const showActionForm = ref(false);
const showMergeStyleMenu = shallowRef(false);
const showActionForm = shallowRef(false);
const mergeButtonStyleClass = computed(() => {
if (mergeForm.value.allOverridableChecksOk) return 'primary';
if (mergeForm.allOverridableChecksOk) return 'primary';
return autoMergeWhenSucceed.value ? 'primary' : 'red';
});
const forceMerge = computed(() => {
return mergeForm.value.canMergeNow && !mergeForm.value.allOverridableChecksOk;
return mergeForm.canMergeNow && !mergeForm.allOverridableChecksOk;
});
watch(mergeStyle, (val) => {
mergeStyleDetail.value = mergeForm.value.mergeStyles.find((e: any) => e.name === val);
mergeStyleDetail.value = mergeForm.mergeStyles.find((e: any) => e.name === val);
for (const elem of document.querySelectorAll('[data-pull-merge-style]')) {
toggleElem(elem, elem.getAttribute('data-pull-merge-style') === val);
}
});
onMounted(() => {
mergeStyleAllowedCount.value = mergeForm.value.mergeStyles.reduce((v: any, msd: any) => v + (msd.allowed ? 1 : 0), 0);
mergeStyleAllowedCount.value = mergeForm.mergeStyles.reduce((v: any, msd: any) => v + (msd.allowed ? 1 : 0), 0);
let mergeStyle = mergeForm.value.mergeStyles.find((e: any) => e.allowed && e.name === mergeForm.value.defaultMergeStyle)?.name;
if (!mergeStyle) mergeStyle = mergeForm.value.mergeStyles.find((e: any) => e.allowed)?.name;
switchMergeStyle(mergeStyle, !mergeForm.value.canMergeNow);
let mergeStyle = mergeForm.mergeStyles.find((e: any) => e.allowed && e.name === mergeForm.defaultMergeStyle)?.name;
if (!mergeStyle) mergeStyle = mergeForm.mergeStyles.find((e: any) => e.allowed)?.name;
switchMergeStyle(mergeStyle, !mergeForm.canMergeNow);
document.addEventListener('mouseup', hideMergeStyleMenu);
});
@ -63,7 +63,7 @@ function hideMergeStyleMenu() {
function toggleActionForm(show: boolean) {
showActionForm.value = show;
if (!show) return;
deleteBranchAfterMerge.value = mergeForm.value.defaultDeleteBranchAfterMerge;
deleteBranchAfterMerge.value = mergeForm.defaultDeleteBranchAfterMerge;
mergeTitleFieldValue.value = mergeStyleDetail.value.mergeTitleFieldText;
mergeMessageFieldValue.value = mergeStyleDetail.value.mergeMessageFieldText;
}
@ -74,7 +74,7 @@ function switchMergeStyle(name: string, autoMerge = false) {
}
function clearMergeMessage() {
mergeMessageFieldValue.value = mergeForm.value.defaultMergeMessage;
mergeMessageFieldValue.value = mergeForm.defaultMergeMessage;
}
</script>

View File

@ -1,9 +1,9 @@
<script lang="ts" setup>
// @ts-expect-error - module exports no types
import {VueBarGraph} from 'vue-bar-graph';
import {computed, onMounted, ref} from 'vue';
import {computed, onMounted, shallowRef, useTemplateRef} from 'vue';
const colors = ref({
const colors = shallowRef({
barColor: 'green',
textColor: 'black',
textAltColor: 'white',
@ -41,8 +41,8 @@ const graphWidth = computed(() => {
return activityTopAuthors.length * 40;
});
const styleElement = ref<HTMLElement | null>(null);
const altStyleElement = ref<HTMLElement | null>(null);
const styleElement = useTemplateRef('styleElement');
const altStyleElement = useTemplateRef('altStyleElement');
onMounted(() => {
const refStyle = window.getComputedStyle(styleElement.value);

View File

@ -23,7 +23,7 @@ import {
import {chartJsColors} from '../utils/color.ts';
import {sleep} from '../utils.ts';
import 'chartjs-adapter-dayjs-4/dist/chartjs-adapter-dayjs-4.esm';
import {onMounted, ref} from 'vue';
import {onMounted, shallowRef} from 'vue';
const {pageData} = window.config;
@ -47,10 +47,10 @@ defineProps<{
};
}>();
const isLoading = ref(false);
const errorText = ref('');
const repoLink = ref(pageData.repoLink || []);
const data = ref<DayData[]>([]);
const isLoading = shallowRef(false);
const errorText = shallowRef('');
const repoLink = pageData.repoLink;
const data = shallowRef<DayData[]>([]);
onMounted(() => {
fetchGraphData();
@ -61,7 +61,7 @@ async function fetchGraphData() {
try {
let response: Response;
do {
response = await GET(`${repoLink.value}/activity/code-frequency/data`);
response = await GET(`${repoLink}/activity/code-frequency/data`);
if (response.status === 202) {
await sleep(1000); // wait for 1 second before retrying
}

View File

@ -21,7 +21,7 @@ import {
import {chartJsColors} from '../utils/color.ts';
import {sleep} from '../utils.ts';
import 'chartjs-adapter-dayjs-4/dist/chartjs-adapter-dayjs-4.esm';
import {onMounted, ref} from 'vue';
import {onMounted, ref, shallowRef} from 'vue';
const {pageData} = window.config;
@ -43,9 +43,9 @@ defineProps<{
};
}>();
const isLoading = ref(false);
const errorText = ref('');
const repoLink = ref(pageData.repoLink || []);
const isLoading = shallowRef(false);
const errorText = shallowRef('');
const repoLink = pageData.repoLink;
const data = ref<DayData[]>([]);
onMounted(() => {
@ -57,7 +57,7 @@ async function fetchGraphData() {
try {
let response: Response;
do {
response = await GET(`${repoLink.value}/activity/recent-commits/data`);
response = await GET(`${repoLink}/activity/recent-commits/data`);
if (response.status === 202) {
await sleep(1000); // wait for 1 second before retrying
}

View File

@ -1,9 +1,9 @@
<script lang="ts" setup>
import ViewFileTreeItem from './ViewFileTreeItem.vue';
import {onMounted, ref} from 'vue';
import {onMounted, useTemplateRef} from 'vue';
import {createViewFileTreeStore} from './ViewFileTreeStore.ts';
const elRoot = ref<HTMLElement | null>(null);
const elRoot = useTemplateRef('elRoot');
const props = defineProps({
repoLink: {type: String, required: true},

View File

@ -1,7 +1,7 @@
<script lang="ts" setup>
import {SvgIcon} from '../svg.ts';
import {isPlainClick} from '../utils/dom.ts';
import {ref} from 'vue';
import {shallowRef} from 'vue';
import {type createViewFileTreeStore} from './ViewFileTreeStore.ts';
type Item = {
@ -20,9 +20,9 @@ const props = defineProps<{
}>();
const store = props.store;
const isLoading = ref(false);
const children = ref(props.item.children);
const collapsed = ref(!props.item.children);
const isLoading = shallowRef(false);
const children = shallowRef(props.item.children);
const collapsed = shallowRef(!props.item.children);
const doLoadChildren = async () => {
collapsed.value = !collapsed.value;

View File

@ -5,7 +5,7 @@ import {confirmModal} from './comp/ConfirmModal.ts';
import type {RequestOpts} from '../types.ts';
import {ignoreAreYouSure} from '../vendor/jquery.are-you-sure.ts';
const {appSubUrl, i18n} = window.config;
const {appSubUrl} = window.config;
// fetchActionDoRedirect does real redirection to bypass the browser's limitations of "location"
// more details are in the backend's fetch-redirect handler
@ -23,11 +23,20 @@ function fetchActionDoRedirect(redirect: string) {
}
async function fetchActionDoRequest(actionElem: HTMLElement, url: string, opt: RequestOpts) {
const showErrorForResponse = (code: number, message: string) => {
showErrorToast(`Error ${code || 'request'}: ${message}`);
};
let respStatus = 0;
let respText = '';
try {
hideToastsAll();
const resp = await request(url, opt);
if (resp.status === 200) {
let {redirect} = await resp.json();
respStatus = resp.status;
respText = await resp.text();
const respJson = JSON.parse(respText);
if (respStatus === 200) {
let {redirect} = respJson;
redirect = redirect || actionElem.getAttribute('data-redirect');
ignoreAreYouSure(actionElem); // ignore the areYouSure check before reloading
if (redirect) {
@ -38,22 +47,19 @@ async function fetchActionDoRequest(actionElem: HTMLElement, url: string, opt: R
return;
}
if (resp.status >= 400 && resp.status < 500) {
const data = await resp.json();
if (respStatus >= 400 && respStatus < 500 && respJson?.errorMessage) {
// the code was quite messy, sometimes the backend uses "err", sometimes it uses "error", and even "user_error"
// but at the moment, as a new approach, we only use "errorMessage" here, backend can use JSONError() to respond.
if (data.errorMessage) {
showErrorToast(data.errorMessage, {useHtmlBody: data.renderFormat === 'html'});
} else {
showErrorToast(`server error: ${resp.status}`);
}
showErrorToast(respJson.errorMessage, {useHtmlBody: respJson.renderFormat === 'html'});
} else {
showErrorToast(`server error: ${resp.status}`);
showErrorForResponse(respStatus, respText);
}
} catch (e) {
if (e.name !== 'AbortError') {
console.error('error when doRequest', e);
showErrorToast(`${i18n.network_error} ${e}`);
if (e.name === 'SyntaxError') {
showErrorForResponse(respStatus, (respText || '').substring(0, 100));
} else if (e.name !== 'AbortError') {
console.error('fetchActionDoRequest error', e);
showErrorForResponse(respStatus, `${e}`);
}
}
actionElem.classList.remove('is-loading', 'loading-icon-2px');