mirror of https://github.com/go-gitea/gitea.git
Compare commits
8 Commits
c02523f914
...
69cd909e0b
| Author | SHA1 | Date |
|---|---|---|
|
|
69cd909e0b | |
|
|
181db69e0c | |
|
|
a46b16f10f | |
|
|
1748045285 | |
|
|
f114c388ff | |
|
|
92c9876892 | |
|
|
f31b5cb14b | |
|
|
4f10849ecd |
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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])))
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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...)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"},
|
||||
})
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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())
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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),
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -130,8 +130,7 @@
|
|||
<span class="only-mobile">{{.SignedUser.Name}}</span>
|
||||
<span class="not-mobile">{{svg "octicon-triangle-down"}}</span>
|
||||
</span>
|
||||
{{/* do not localize it, here it needs the fixed length (width) to make UI comfortable */}}
|
||||
{{if .IsAdmin}}<span class="navbar-profile-admin">admin</span>{{end}}
|
||||
{{if .IsAdmin}}<span class="navbar-profile-admin">{{svg "octicon-server" 11}}</span>{{end}}
|
||||
<div class="menu user-menu">
|
||||
<div class="header">
|
||||
{{ctx.Locale.Tr "signed_in_as"}} <strong>{{.SignedUser.Name}}</strong>
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -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"}}
|
||||
|
|
|
|||
|
|
@ -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}}
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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" .}}
|
||||
|
|
@ -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"}}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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}}
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -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}}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -101,17 +101,22 @@
|
|||
}
|
||||
}
|
||||
|
||||
#navbar .ui.dropdown.active .navbar-profile-admin {
|
||||
background: var(--color-nav-hover-bg);
|
||||
border-color: var(--color-nav-hover-bg);
|
||||
}
|
||||
|
||||
#navbar .ui.dropdown .navbar-profile-admin {
|
||||
display: block;
|
||||
position: absolute;
|
||||
font-size: 9px;
|
||||
font-weight: var(--font-weight-bold);
|
||||
color: var(--color-nav-bg);
|
||||
background: var(--color-primary);
|
||||
padding: 2px 3px;
|
||||
color: var(--color-text);
|
||||
background: var(--color-nav-bg);
|
||||
border: 1px solid var(--color-nav-bg);
|
||||
padding: 2px;
|
||||
border-radius: 10px;
|
||||
top: -1px;
|
||||
left: 18px;
|
||||
top: -0.5px;
|
||||
left: 28px;
|
||||
line-height: 0;
|
||||
}
|
||||
|
||||
#navbar a.item:hover .notification_count,
|
||||
|
|
|
|||
|
|
@ -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 */
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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>) => {
|
||||
|
|
|
|||
|
|
@ -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> }> = {
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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},
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
|
|
|
|||
Loading…
Reference in New Issue