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.
|
// CanEnableEditor returns true if repository meets the requirements of web editor.
|
||||||
func (repo *Repository) CanEnableEditor() bool {
|
func (repo *Repository) CanEnableEditor() bool {
|
||||||
return !repo.IsMirror
|
return !repo.IsMirror && !repo.IsArchived
|
||||||
}
|
}
|
||||||
|
|
||||||
// DescriptionHTML does special handles to description and return HTML string.
|
// DescriptionHTML does special handles to description and return HTML string.
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@
|
||||||
package markup
|
package markup
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"html/template"
|
||||||
"io"
|
"io"
|
||||||
"net/url"
|
"net/url"
|
||||||
"regexp"
|
"regexp"
|
||||||
|
|
@ -92,9 +93,9 @@ func (st *Sanitizer) createDefaultPolicy() *bluemonday.Policy {
|
||||||
return policy
|
return policy
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sanitize takes a string that contains a HTML fragment or document and applies policy whitelist.
|
// Sanitize use default sanitizer policy to sanitize a string
|
||||||
func Sanitize(s string) string {
|
func Sanitize(s string) template.HTML {
|
||||||
return GetDefaultSanitizer().defaultPolicy.Sanitize(s)
|
return template.HTML(GetDefaultSanitizer().defaultPolicy.Sanitize(s))
|
||||||
}
|
}
|
||||||
|
|
||||||
// SanitizeReader sanitizes a Reader
|
// SanitizeReader sanitizes a Reader
|
||||||
|
|
|
||||||
|
|
@ -69,6 +69,6 @@ func TestSanitizer(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
for i := 0; i < len(testCases); i += 2 {
|
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))
|
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 {
|
func SanitizeHTML(s string) template.HTML {
|
||||||
return template.HTML(markup.Sanitize(s))
|
return markup.Sanitize(s)
|
||||||
}
|
}
|
||||||
|
|
||||||
func htmlEscape(s any) template.HTML {
|
func htmlEscape(s any) template.HTML {
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ package web
|
||||||
import (
|
import (
|
||||||
"net/http"
|
"net/http"
|
||||||
"regexp"
|
"regexp"
|
||||||
|
"slices"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"code.gitea.io/gitea/modules/container"
|
"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)
|
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.
|
// 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)
|
// 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.
|
// 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).
|
// 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) {
|
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...))
|
g.matchers = append(g.matchers, newRouterPathMatcher(methods, pattern, h...))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -96,8 +107,8 @@ func isValidMethod(name string) bool {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
func newRouterPathMatcher(methods, pattern string, h ...any) *routerPathMatcher {
|
func newRouterPathMatcher(methods string, patternRegexp *RouterPathGroupPattern, h ...any) *routerPathMatcher {
|
||||||
middlewares, handlerFunc := wrapMiddlewareAndHandler(nil, h)
|
middlewares, handlerFunc := wrapMiddlewareAndHandler(patternRegexp.middlewares, h)
|
||||||
p := &routerPathMatcher{methods: make(container.Set[string]), middlewares: middlewares, handlerFunc: handlerFunc}
|
p := &routerPathMatcher{methods: make(container.Set[string]), middlewares: middlewares, handlerFunc: handlerFunc}
|
||||||
for method := range strings.SplitSeq(methods, ",") {
|
for method := range strings.SplitSeq(methods, ",") {
|
||||||
method = strings.TrimSpace(method)
|
method = strings.TrimSpace(method)
|
||||||
|
|
@ -106,19 +117,25 @@ func newRouterPathMatcher(methods, pattern string, h ...any) *routerPathMatcher
|
||||||
}
|
}
|
||||||
p.methods.Add(method)
|
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{'^'}
|
re := []byte{'^'}
|
||||||
lastEnd := 0
|
lastEnd := 0
|
||||||
for lastEnd < len(pattern) {
|
for lastEnd < len(pattern) {
|
||||||
start := strings.IndexByte(pattern[lastEnd:], '<')
|
start := strings.IndexByte(pattern[lastEnd:], '<')
|
||||||
if start == -1 {
|
if start == -1 {
|
||||||
re = append(re, pattern[lastEnd:]...)
|
re = append(re, regexp.QuoteMeta(pattern[lastEnd:])...)
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
end := strings.IndexByte(pattern[lastEnd+start:], '>')
|
end := strings.IndexByte(pattern[lastEnd+start:], '>')
|
||||||
if end == -1 {
|
if end == -1 {
|
||||||
panic("invalid pattern: " + pattern)
|
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], ":")
|
partName, partExp, _ := strings.Cut(pattern[lastEnd+start+1:lastEnd+start+end], ":")
|
||||||
lastEnd += start + end + 1
|
lastEnd += start + end + 1
|
||||||
|
|
||||||
|
|
@ -140,7 +157,10 @@ func newRouterPathMatcher(methods, pattern string, h ...any) *routerPathMatcher
|
||||||
p.params = append(p.params, param)
|
p.params = append(p.params, param)
|
||||||
}
|
}
|
||||||
re = append(re, '$')
|
re = append(re, '$')
|
||||||
reStr := string(re)
|
p.re = regexp.MustCompile(string(re))
|
||||||
p.re = regexp.MustCompile(reStr)
|
|
||||||
return p
|
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) {
|
testProcess := func(pattern, uri string, expectedPathParams map[string]string) {
|
||||||
chiCtx := chi.NewRouteContext()
|
chiCtx := chi.NewRouteContext()
|
||||||
chiCtx.RouteMethod = "GET"
|
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.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)
|
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
|
recorder.Body = buff
|
||||||
|
|
||||||
type resultStruct struct {
|
type resultStruct struct {
|
||||||
method string
|
method string
|
||||||
pathParams map[string]string
|
pathParams map[string]string
|
||||||
handlerMark string
|
handlerMarks []string
|
||||||
}
|
}
|
||||||
var res resultStruct
|
|
||||||
|
|
||||||
|
var res resultStruct
|
||||||
h := func(optMark ...string) func(resp http.ResponseWriter, req *http.Request) {
|
h := func(optMark ...string) func(resp http.ResponseWriter, req *http.Request) {
|
||||||
mark := util.OptionalArg(optMark, "")
|
mark := util.OptionalArg(optMark, "")
|
||||||
return func(resp http.ResponseWriter, req *http.Request) {
|
return func(resp http.ResponseWriter, req *http.Request) {
|
||||||
res.method = req.Method
|
res.method = req.Method
|
||||||
res.pathParams = chiURLParamsToMap(chi.RouteContext(req.Context()))
|
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) {
|
if stop := req.FormValue("stop"); stop != "" && (mark == "" || mark == stop) {
|
||||||
h(stop)(resp, req)
|
h(stop)(resp, req)
|
||||||
resp.WriteHeader(http.StatusOK)
|
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.Delete("", h())
|
||||||
})
|
})
|
||||||
m.PathGroup("/*", func(g *RouterPathGroup) {
|
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"))
|
}, stopMark("s1"))
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
@ -126,31 +130,31 @@ func TestRouter(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
t.Run("RootRouter", func(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{
|
testRoute(t, "GET /the-user/the-repo/pulls", resultStruct{
|
||||||
method: "GET",
|
method: "GET",
|
||||||
pathParams: map[string]string{"username": "the-user", "reponame": "the-repo", "type": "pulls"},
|
pathParams: map[string]string{"username": "the-user", "reponame": "the-repo", "type": "pulls"},
|
||||||
handlerMark: "list-issues-b",
|
handlerMarks: []string{"list-issues-b"},
|
||||||
})
|
})
|
||||||
testRoute(t, "GET /the-user/the-repo/issues/123", resultStruct{
|
testRoute(t, "GET /the-user/the-repo/issues/123", resultStruct{
|
||||||
method: "GET",
|
method: "GET",
|
||||||
pathParams: map[string]string{"username": "the-user", "reponame": "the-repo", "type": "issues", "index": "123"},
|
pathParams: map[string]string{"username": "the-user", "reponame": "the-repo", "type": "issues", "index": "123"},
|
||||||
handlerMark: "view-issue",
|
handlerMarks: []string{"view-issue"},
|
||||||
})
|
})
|
||||||
testRoute(t, "GET /the-user/the-repo/issues/123?stop=hijack", resultStruct{
|
testRoute(t, "GET /the-user/the-repo/issues/123?stop=hijack", resultStruct{
|
||||||
method: "GET",
|
method: "GET",
|
||||||
pathParams: map[string]string{"username": "the-user", "reponame": "the-repo", "type": "issues", "index": "123"},
|
pathParams: map[string]string{"username": "the-user", "reponame": "the-repo", "type": "issues", "index": "123"},
|
||||||
handlerMark: "hijack",
|
handlerMarks: []string{"hijack"},
|
||||||
})
|
})
|
||||||
testRoute(t, "POST /the-user/the-repo/issues/123/update", resultStruct{
|
testRoute(t, "POST /the-user/the-repo/issues/123/update", resultStruct{
|
||||||
method: "POST",
|
method: "POST",
|
||||||
pathParams: map[string]string{"username": "the-user", "reponame": "the-repo", "index": "123"},
|
pathParams: map[string]string{"username": "the-user", "reponame": "the-repo", "index": "123"},
|
||||||
handlerMark: "update-issue",
|
handlerMarks: []string{"update-issue"},
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("Sub Router", func(t *testing.T) {
|
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{
|
testRoute(t, "GET /api/v1/repos/the-user/the-repo/branches", resultStruct{
|
||||||
method: "GET",
|
method: "GET",
|
||||||
pathParams: map[string]string{"username": "the-user", "reponame": "the-repo"},
|
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) {
|
t.Run("MatchPath", func(t *testing.T) {
|
||||||
testRoute(t, "GET /api/v1/repos/the-user/the-repo/branches/d1/d2/fn", resultStruct{
|
testRoute(t, "GET /api/v1/repos/the-user/the-repo/branches/d1/d2/fn", resultStruct{
|
||||||
method: "GET",
|
method: "GET",
|
||||||
pathParams: map[string]string{"username": "the-user", "reponame": "the-repo", "*": "d1/d2/fn", "dir": "d1/d2", "file": "fn"},
|
pathParams: map[string]string{"username": "the-user", "reponame": "the-repo", "*": "d1/d2/fn", "dir": "d1/d2", "file": "fn"},
|
||||||
handlerMark: "match-path",
|
handlerMarks: []string{"s1", "s2", "s3", "match-path"},
|
||||||
})
|
})
|
||||||
testRoute(t, "GET /api/v1/repos/the-user/the-repo/branches/d1%2fd2/fn", resultStruct{
|
testRoute(t, "GET /api/v1/repos/the-user/the-repo/branches/d1%2fd2/fn", resultStruct{
|
||||||
method: "GET",
|
method: "GET",
|
||||||
pathParams: map[string]string{"username": "the-user", "reponame": "the-repo", "*": "d1%2fd2/fn", "dir": "d1%2fd2", "file": "fn"},
|
pathParams: map[string]string{"username": "the-user", "reponame": "the-repo", "*": "d1%2fd2/fn", "dir": "d1%2fd2", "file": "fn"},
|
||||||
handlerMark: "match-path",
|
handlerMarks: []string{"s1", "s2", "s3", "match-path"},
|
||||||
})
|
})
|
||||||
testRoute(t, "GET /api/v1/repos/the-user/the-repo/branches/d1/d2/000", resultStruct{
|
testRoute(t, "GET /api/v1/repos/the-user/the-repo/branches/d1/d2/000", resultStruct{
|
||||||
method: "GET",
|
method: "GET",
|
||||||
pathParams: map[string]string{"reponame": "the-repo", "username": "the-user", "*": "d1/d2/000"},
|
pathParams: map[string]string{"reponame": "the-repo", "username": "the-user", "*": "d1/d2/000"},
|
||||||
handlerMark: "not-found:/api/v1",
|
handlerMarks: []string{"s1", "not-found:/api/v1"},
|
||||||
})
|
})
|
||||||
|
|
||||||
testRoute(t, "GET /api/v1/repos/the-user/the-repo/branches/d1/d2/fn?stop=s1", resultStruct{
|
testRoute(t, "GET /api/v1/repos/the-user/the-repo/branches/d1/d2/fn?stop=s1", resultStruct{
|
||||||
method: "GET",
|
method: "GET",
|
||||||
pathParams: map[string]string{"username": "the-user", "reponame": "the-repo", "*": "d1/d2/fn"},
|
pathParams: map[string]string{"username": "the-user", "reponame": "the-repo", "*": "d1/d2/fn"},
|
||||||
handlerMark: "s1",
|
handlerMarks: []string{"s1"},
|
||||||
})
|
})
|
||||||
|
|
||||||
testRoute(t, "GET /api/v1/repos/the-user/the-repo/branches/d1/d2/fn?stop=s2", resultStruct{
|
testRoute(t, "GET /api/v1/repos/the-user/the-repo/branches/d1/d2/fn?stop=s2", resultStruct{
|
||||||
method: "GET",
|
method: "GET",
|
||||||
pathParams: map[string]string{"username": "the-user", "reponame": "the-repo", "*": "d1/d2/fn", "dir": "d1/d2", "file": "fn"},
|
pathParams: map[string]string{"username": "the-user", "reponame": "the-repo", "*": "d1/d2/fn", "dir": "d1/d2", "file": "fn"},
|
||||||
handlerMark: "s2",
|
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 = Failed to commit changes.
|
||||||
editor.failed_to_commit_summary = Error Message:
|
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.desc = Browse source code change history.
|
||||||
commits.commits = Commits
|
commits.commits = Commits
|
||||||
commits.no_commits = No commits in common. "%s" and "%s" have entirely different histories.
|
commits.no_commits = No commits in common. "%s" and "%s" have entirely different histories.
|
||||||
|
|
|
||||||
|
|
@ -5,8 +5,6 @@ package packages
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"net/http"
|
"net/http"
|
||||||
"regexp"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
auth_model "code.gitea.io/gitea/models/auth"
|
auth_model "code.gitea.io/gitea/models/auth"
|
||||||
"code.gitea.io/gitea/models/perm"
|
"code.gitea.io/gitea/models/perm"
|
||||||
|
|
@ -282,42 +280,10 @@ func CommonRoutes() *web.Router {
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
}, reqPackageAccess(perm.AccessModeRead))
|
}, reqPackageAccess(perm.AccessModeRead))
|
||||||
r.Group("/conda", func() {
|
r.PathGroup("/conda/*", func(g *web.RouterPathGroup) {
|
||||||
var (
|
g.MatchPath("GET", "/<architecture>/<filename>", conda.ListOrGetPackages)
|
||||||
downloadPattern = regexp.MustCompile(`\A(.+/)?(.+)/((?:[^/]+(?:\.tar\.bz2|\.conda))|(?:current_)?repodata\.json(?:\.bz2)?)\z`)
|
g.MatchPath("GET", "/<channel:*>/<architecture>/<filename>", conda.ListOrGetPackages)
|
||||||
uploadPattern = regexp.MustCompile(`\A(.+/)?([^/]+(?:\.tar\.bz2|\.conda))\z`)
|
g.MatchPath("PUT", "/<channel:*>/<filename>", reqPackageAccess(perm.AccessModeWrite), conda.UploadPackageFile)
|
||||||
)
|
|
||||||
|
|
||||||
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)
|
|
||||||
})
|
|
||||||
}, reqPackageAccess(perm.AccessModeRead))
|
}, reqPackageAccess(perm.AccessModeRead))
|
||||||
r.Group("/cran", func() {
|
r.Group("/cran", func() {
|
||||||
r.Group("/src", func() {
|
r.Group("/src", func() {
|
||||||
|
|
@ -358,60 +324,15 @@ func CommonRoutes() *web.Router {
|
||||||
}, reqPackageAccess(perm.AccessModeRead))
|
}, reqPackageAccess(perm.AccessModeRead))
|
||||||
r.Group("/go", func() {
|
r.Group("/go", func() {
|
||||||
r.Put("/upload", reqPackageAccess(perm.AccessModeWrite), goproxy.UploadPackage)
|
r.Put("/upload", reqPackageAccess(perm.AccessModeWrite), goproxy.UploadPackage)
|
||||||
r.Get("/sumdb/sum.golang.org/supported", func(ctx *context.Context) {
|
r.Get("/sumdb/sum.golang.org/supported", http.NotFound)
|
||||||
ctx.Status(http.StatusNotFound)
|
|
||||||
})
|
|
||||||
|
|
||||||
// Manual mapping of routes because the package name contains slashes which chi does not support
|
|
||||||
// https://go.dev/ref/mod#goproxy-protocol
|
// https://go.dev/ref/mod#goproxy-protocol
|
||||||
r.Get("/*", func(ctx *context.Context) {
|
r.PathGroup("/*", func(g *web.RouterPathGroup) {
|
||||||
path := ctx.PathParam("*")
|
g.MatchPath("GET", "/<name:*>/@<version:latest>", goproxy.PackageVersionMetadata)
|
||||||
|
g.MatchPath("GET", "/<name:*>/@v/list", goproxy.EnumeratePackageVersions)
|
||||||
if strings.HasSuffix(path, "/@latest") {
|
g.MatchPath("GET", "/<name:*>/@v/<version>.zip", goproxy.DownloadPackageFile)
|
||||||
ctx.SetPathParam("name", path[:len(path)-len("/@latest")])
|
g.MatchPath("GET", "/<name:*>/@v/<version>.info", goproxy.PackageVersionMetadata)
|
||||||
ctx.SetPathParam("version", "latest")
|
g.MatchPath("GET", "/<name:*>/@v/<version>.mod", goproxy.PackageVersionGoModContent)
|
||||||
|
|
||||||
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)
|
|
||||||
})
|
})
|
||||||
}, reqPackageAccess(perm.AccessModeRead))
|
}, reqPackageAccess(perm.AccessModeRead))
|
||||||
r.Group("/generic", func() {
|
r.Group("/generic", func() {
|
||||||
|
|
@ -532,82 +453,24 @@ func CommonRoutes() *web.Router {
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
}, reqPackageAccess(perm.AccessModeRead))
|
}, reqPackageAccess(perm.AccessModeRead))
|
||||||
|
|
||||||
r.Group("/pypi", func() {
|
r.Group("/pypi", func() {
|
||||||
r.Post("/", reqPackageAccess(perm.AccessModeWrite), pypi.UploadPackageFile)
|
r.Post("/", reqPackageAccess(perm.AccessModeWrite), pypi.UploadPackageFile)
|
||||||
r.Get("/files/{id}/{version}/{filename}", pypi.DownloadPackageFile)
|
r.Get("/files/{id}/{version}/{filename}", pypi.DownloadPackageFile)
|
||||||
r.Get("/simple/{id}", pypi.PackageMetadata)
|
r.Get("/simple/{id}", pypi.PackageMetadata)
|
||||||
}, reqPackageAccess(perm.AccessModeRead))
|
}, reqPackageAccess(perm.AccessModeRead))
|
||||||
r.Group("/rpm", func() {
|
|
||||||
r.Group("/repository.key", func() {
|
|
||||||
r.Head("", rpm.GetRepositoryKey)
|
|
||||||
r.Get("", rpm.GetRepositoryKey)
|
|
||||||
})
|
|
||||||
|
|
||||||
var (
|
r.Methods("HEAD,GET", "/rpm.repo", reqPackageAccess(perm.AccessModeRead), rpm.GetRepositoryConfig)
|
||||||
repoPattern = regexp.MustCompile(`\A(.*?)\.repo\z`)
|
r.PathGroup("/rpm/*", func(g *web.RouterPathGroup) {
|
||||||
uploadPattern = regexp.MustCompile(`\A(.*?)/upload\z`)
|
g.MatchPath("HEAD,GET", "/repository.key", rpm.GetRepositoryKey)
|
||||||
filePattern = regexp.MustCompile(`\A(.*?)/package/([^/]+)/([^/]+)/([^/]+)(?:/([^/]+\.rpm)|)\z`)
|
g.MatchPath("HEAD,GET", "/<group:*>.repo", rpm.GetRepositoryConfig)
|
||||||
repoFilePattern = regexp.MustCompile(`\A(.*?)/repodata/([^/]+)\z`)
|
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)
|
||||||
r.Methods("HEAD,GET,PUT,DELETE", "*", func(ctx *context.Context) {
|
g.MatchPath("HEAD,GET", "/<group:*>/package/<name>/<version>/<architecture>", rpm.DownloadPackageFile)
|
||||||
path := ctx.PathParam("*")
|
g.MatchPath("DELETE", "/<group:*>/package/<name>/<version>/<architecture>", reqPackageAccess(perm.AccessModeWrite), rpm.DeletePackageFile)
|
||||||
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)
|
|
||||||
})
|
|
||||||
}, reqPackageAccess(perm.AccessModeRead))
|
}, reqPackageAccess(perm.AccessModeRead))
|
||||||
|
|
||||||
r.Group("/rubygems", func() {
|
r.Group("/rubygems", func() {
|
||||||
r.Get("/specs.4.8.gz", rubygems.EnumeratePackages)
|
r.Get("/specs.4.8.gz", rubygems.EnumeratePackages)
|
||||||
r.Get("/latest_specs.4.8.gz", rubygems.EnumeratePackagesLatest)
|
r.Get("/latest_specs.4.8.gz", rubygems.EnumeratePackagesLatest)
|
||||||
|
|
@ -621,6 +484,7 @@ func CommonRoutes() *web.Router {
|
||||||
r.Delete("/yank", rubygems.DeletePackage)
|
r.Delete("/yank", rubygems.DeletePackage)
|
||||||
}, reqPackageAccess(perm.AccessModeWrite))
|
}, reqPackageAccess(perm.AccessModeWrite))
|
||||||
}, reqPackageAccess(perm.AccessModeRead))
|
}, reqPackageAccess(perm.AccessModeRead))
|
||||||
|
|
||||||
r.Group("/swift", func() {
|
r.Group("/swift", func() {
|
||||||
r.Group("", func() { // Needs to be unauthenticated.
|
r.Group("", func() { // Needs to be unauthenticated.
|
||||||
r.Post("", swift.CheckAuthenticate)
|
r.Post("", swift.CheckAuthenticate)
|
||||||
|
|
@ -632,31 +496,12 @@ func CommonRoutes() *web.Router {
|
||||||
r.Get("", swift.EnumeratePackageVersions)
|
r.Get("", swift.EnumeratePackageVersions)
|
||||||
r.Get(".json", swift.EnumeratePackageVersions)
|
r.Get(".json", swift.EnumeratePackageVersions)
|
||||||
}, swift.CheckAcceptMediaType(swift.AcceptJSON))
|
}, swift.CheckAcceptMediaType(swift.AcceptJSON))
|
||||||
r.Group("/{version}", func() {
|
r.PathGroup("/*", func(g *web.RouterPathGroup) {
|
||||||
r.Get("/Package.swift", swift.CheckAcceptMediaType(swift.AcceptSwift), swift.DownloadManifest)
|
g.MatchPath("GET", "/<version>.json", swift.CheckAcceptMediaType(swift.AcceptJSON), swift.PackageVersionMetadata)
|
||||||
r.Put("", reqPackageAccess(perm.AccessModeWrite), swift.CheckAcceptMediaType(swift.AcceptJSON), swift.UploadPackageFile)
|
g.MatchPath("GET", "/<version>.zip", swift.CheckAcceptMediaType(swift.AcceptZip), swift.DownloadPackageFile)
|
||||||
r.Get("", func(ctx *context.Context) {
|
g.MatchPath("GET", "/<version>/Package.swift", swift.CheckAcceptMediaType(swift.AcceptSwift), swift.DownloadManifest)
|
||||||
// Can't use normal routes here: https://github.com/go-chi/chi/issues/781
|
g.MatchPath("GET", "/<version>", swift.CheckAcceptMediaType(swift.AcceptJSON), swift.PackageVersionMetadata)
|
||||||
|
g.MatchPath("PUT", "/<version>", reqPackageAccess(perm.AccessModeWrite), swift.CheckAcceptMediaType(swift.AcceptJSON), swift.UploadPackageFile)
|
||||||
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.Get("/identifiers", swift.CheckAcceptMediaType(swift.AcceptJSON), swift.LookupPackageIdentifiers)
|
r.Get("/identifiers", swift.CheckAcceptMediaType(swift.AcceptJSON), swift.LookupPackageIdentifiers)
|
||||||
|
|
@ -705,18 +550,13 @@ func ContainerRoutes() *web.Router {
|
||||||
r.PathGroup("/*", func(g *web.RouterPathGroup) {
|
r.PathGroup("/*", func(g *web.RouterPathGroup) {
|
||||||
g.MatchPath("POST", "/<image:*>/blobs/uploads", reqPackageAccess(perm.AccessModeWrite), container.VerifyImageName, container.PostBlobsUploads)
|
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", "/<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 {
|
patternBlobsUploadsUUID := g.PatternRegexp(`/<image:*>/blobs/uploads/<uuid:[-.=\w]+>`, reqPackageAccess(perm.AccessModeWrite), container.VerifyImageName)
|
||||||
case http.MethodGet:
|
g.MatchPattern("GET", patternBlobsUploadsUUID, container.GetBlobsUpload)
|
||||||
container.GetBlobsUpload(ctx)
|
g.MatchPattern("PATCH", patternBlobsUploadsUUID, container.PatchBlobsUpload)
|
||||||
case http.MethodPatch:
|
g.MatchPattern("PUT", patternBlobsUploadsUUID, container.PutBlobsUpload)
|
||||||
container.PatchBlobsUpload(ctx)
|
g.MatchPattern("DELETE", patternBlobsUploadsUUID, container.DeleteBlobsUpload)
|
||||||
case http.MethodPut:
|
|
||||||
container.PutBlobsUpload(ctx)
|
|
||||||
default: /* DELETE */
|
|
||||||
container.DeleteBlobsUpload(ctx)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
g.MatchPath("HEAD", `/<image:*>/blobs/<digest>`, container.VerifyImageName, container.HeadBlob)
|
g.MatchPath("HEAD", `/<image:*>/blobs/<digest>`, container.VerifyImageName, container.HeadBlob)
|
||||||
g.MatchPath("GET", `/<image:*>/blobs/<digest>`, container.VerifyImageName, container.GetBlob)
|
g.MatchPath("GET", `/<image:*>/blobs/<digest>`, container.VerifyImageName, container.GetBlob)
|
||||||
g.MatchPath("DELETE", `/<image:*>/blobs/<digest>`, container.VerifyImageName, reqPackageAccess(perm.AccessModeWrite), container.DeleteBlob)
|
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) {
|
func EnumeratePackages(ctx *context.Context) {
|
||||||
type Info struct {
|
type Info struct {
|
||||||
Subdir string `json:"subdir"`
|
Subdir string `json:"subdir"`
|
||||||
|
|
@ -174,6 +192,12 @@ func EnumeratePackages(ctx *context.Context) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func UploadPackageFile(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()
|
upload, needToClose, err := ctx.UploadStream()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
apiError(ctx, http.StatusInternalServerError, err)
|
apiError(ctx, http.StatusInternalServerError, err)
|
||||||
|
|
@ -191,7 +215,7 @@ func UploadPackageFile(ctx *context.Context) {
|
||||||
defer buf.Close()
|
defer buf.Close()
|
||||||
|
|
||||||
var pck *conda_module.Package
|
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)
|
pck, err = conda_module.ParsePackageBZ2(buf)
|
||||||
} else {
|
} else {
|
||||||
pck, err = conda_module.ParsePackageConda(buf, buf.Size())
|
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 {
|
func containerGlobalLockKey(piOwnerID int64, piName, usage string) string {
|
||||||
return fmt.Sprintf("pkg_%d_container_%s", piOwnerID, strings.ToLower(piName))
|
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) {
|
func getOrCreateUploadVersion(ctx context.Context, pi *packages_service.PackageInfo) (*packages_model.PackageVersion, error) {
|
||||||
var uploadVersion *packages_model.PackageVersion
|
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 {
|
if err != nil {
|
||||||
return nil, err
|
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 {
|
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 {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -32,7 +32,7 @@ import (
|
||||||
packages_service "code.gitea.io/gitea/services/packages"
|
packages_service "code.gitea.io/gitea/services/packages"
|
||||||
container_service "code.gitea.io/gitea/services/packages/container"
|
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
|
// maximum size of a container manifest
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,7 @@ import (
|
||||||
packages_model "code.gitea.io/gitea/models/packages"
|
packages_model "code.gitea.io/gitea/models/packages"
|
||||||
container_model "code.gitea.io/gitea/models/packages/container"
|
container_model "code.gitea.io/gitea/models/packages/container"
|
||||||
user_model "code.gitea.io/gitea/models/user"
|
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/json"
|
||||||
"code.gitea.io/gitea/modules/log"
|
"code.gitea.io/gitea/modules/log"
|
||||||
packages_module "code.gitea.io/gitea/modules/packages"
|
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) {
|
if container_module.IsMediaTypeImageManifest(mci.MediaType) {
|
||||||
return processOciImageManifest(ctx, mci, buf)
|
return processOciImageManifest(ctx, mci, buf)
|
||||||
} else if container_module.IsMediaTypeImageIndex(mci.MediaType) {
|
} else if container_module.IsMediaTypeImageIndex(mci.MediaType) {
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,7 @@ import (
|
||||||
"code.gitea.io/gitea/modules/charset"
|
"code.gitea.io/gitea/modules/charset"
|
||||||
"code.gitea.io/gitea/modules/git"
|
"code.gitea.io/gitea/modules/git"
|
||||||
"code.gitea.io/gitea/modules/httplib"
|
"code.gitea.io/gitea/modules/httplib"
|
||||||
|
"code.gitea.io/gitea/modules/log"
|
||||||
"code.gitea.io/gitea/modules/markup"
|
"code.gitea.io/gitea/modules/markup"
|
||||||
"code.gitea.io/gitea/modules/setting"
|
"code.gitea.io/gitea/modules/setting"
|
||||||
"code.gitea.io/gitea/modules/templates"
|
"code.gitea.io/gitea/modules/templates"
|
||||||
|
|
@ -39,7 +40,7 @@ const (
|
||||||
editorCommitChoiceNewBranch string = "commit-to-new-branch"
|
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)
|
cleanedTreePath := files_service.CleanGitTreePath(ctx.Repo.TreePath)
|
||||||
if cleanedTreePath != 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))
|
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
|
redirectTo += "?" + ctx.Req.URL.RawQuery
|
||||||
}
|
}
|
||||||
ctx.Redirect(redirectTo)
|
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 {
|
if err != nil {
|
||||||
ctx.ServerError("PrepareCommitFormBehaviors", err)
|
ctx.ServerError("PrepareCommitFormOptions", err)
|
||||||
return
|
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["BranchLink"] = ctx.Repo.RepoLink + "/src/" + ctx.Repo.RefTypeNameSubURL()
|
||||||
ctx.Data["TreePath"] = ctx.Repo.TreePath
|
ctx.Data["TreePath"] = ctx.Repo.TreePath
|
||||||
ctx.Data["CommitFormBehaviors"] = commitFormBehaviors
|
ctx.Data["CommitFormOptions"] = commitFormOptions
|
||||||
|
|
||||||
// for online editor
|
// for online editor
|
||||||
ctx.Data["PreviewableExtensions"] = strings.Join(markup.PreviewableExtensions(), ",")
|
ctx.Data["PreviewableExtensions"] = strings.Join(markup.PreviewableExtensions(), ",")
|
||||||
|
|
@ -69,25 +80,27 @@ func prepareEditorCommitFormOptions(ctx *context.Context, editorAction string) {
|
||||||
// form fields
|
// form fields
|
||||||
ctx.Data["commit_summary"] = ""
|
ctx.Data["commit_summary"] = ""
|
||||||
ctx.Data["commit_message"] = ""
|
ctx.Data["commit_message"] = ""
|
||||||
ctx.Data["commit_choice"] = util.Iif(commitFormBehaviors.CanCommitToBranch, editorCommitChoiceDirect, editorCommitChoiceNewBranch)
|
ctx.Data["commit_choice"] = util.Iif(commitFormOptions.CanCommitToBranch, editorCommitChoiceDirect, editorCommitChoiceNewBranch)
|
||||||
ctx.Data["new_branch_name"] = getUniquePatchBranchName(ctx, ctx.Doer.LowerName, ctx.Repo.Repository)
|
ctx.Data["new_branch_name"] = getUniquePatchBranchName(ctx, ctx.Doer.LowerName, commitFormOptions.TargetRepo)
|
||||||
ctx.Data["last_commit"] = ctx.Repo.CommitID
|
ctx.Data["last_commit"] = ctx.Repo.CommitID
|
||||||
|
return commitFormOptions
|
||||||
}
|
}
|
||||||
|
|
||||||
func prepareTreePathFieldsAndPaths(ctx *context.Context, treePath string) {
|
func prepareTreePathFieldsAndPaths(ctx *context.Context, treePath string) {
|
||||||
// show the tree path fields in the "breadcrumb" and help users to edit the target tree path
|
// 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 {
|
type preparedEditorCommitForm[T any] struct {
|
||||||
form T
|
form T
|
||||||
commonForm *forms.CommitCommonForm
|
commonForm *forms.CommitCommonForm
|
||||||
CommitFormBehaviors *context.CommitFormBehaviors
|
CommitFormOptions *context.CommitFormOptions
|
||||||
TargetBranchName string
|
OldBranchName string
|
||||||
GitCommitter *files_service.IdentityOptions
|
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)
|
commitMessage := util.IfZero(strings.TrimSpace(f.commonForm.CommitSummary), defaultCommitMessage)
|
||||||
if body := strings.TrimSpace(f.commonForm.CommitMessage); body != "" {
|
if body := strings.TrimSpace(f.commonForm.CommitMessage); body != "" {
|
||||||
commitMessage += "\n\n" + body
|
commitMessage += "\n\n" + body
|
||||||
|
|
@ -95,7 +108,7 @@ func (f *parsedEditorCommitForm[T]) GetCommitMessage(defaultCommitMessage string
|
||||||
return commitMessage
|
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)
|
form := web.GetForm(ctx).(T)
|
||||||
if ctx.HasError() {
|
if ctx.HasError() {
|
||||||
ctx.JSONError(ctx.GetErrMsg())
|
ctx.JSONError(ctx.GetErrMsg())
|
||||||
|
|
@ -105,15 +118,22 @@ func parseEditorCommitSubmittedForm[T forms.CommitCommonFormInterface](ctx *cont
|
||||||
commonForm := form.GetCommitCommonForm()
|
commonForm := form.GetCommitCommonForm()
|
||||||
commonForm.TreePath = files_service.CleanGitTreePath(commonForm.TreePath)
|
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 {
|
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
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// check commit behavior
|
// check commit behavior
|
||||||
targetBranchName := util.Iif(commonForm.CommitChoice == editorCommitChoiceNewBranch, commonForm.NewBranchName, ctx.Repo.BranchName)
|
fromBaseBranch := ctx.FormString("from_base_branch")
|
||||||
if targetBranchName == ctx.Repo.BranchName && !commitFormBehaviors.CanCommitToBranch {
|
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))
|
ctx.JSONError(ctx.Tr("repo.editor.cannot_commit_to_protected_branch", targetBranchName))
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
@ -125,28 +145,63 @@ func parseEditorCommitSubmittedForm[T forms.CommitCommonFormInterface](ctx *cont
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
return &parsedEditorCommitForm[T]{
|
if commitToNewBranch {
|
||||||
form: form,
|
// if target branch exists, we should stop
|
||||||
commonForm: commonForm,
|
targetBranchExists, err := git_model.IsBranchExist(ctx, commitFormOptions.TargetRepo.ID, targetBranchName)
|
||||||
CommitFormBehaviors: commitFormBehaviors,
|
if err != nil {
|
||||||
TargetBranchName: targetBranchName,
|
ctx.ServerError("IsBranchExist", err)
|
||||||
GitCommitter: gitCommitter,
|
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
|
// 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 {
|
if parsed.commonForm.CommitChoice == editorCommitChoiceNewBranch {
|
||||||
// Redirect to a pull request when possible
|
// Redirect to a pull request when possible
|
||||||
redirectToPullRequest := false
|
redirectToPullRequest := false
|
||||||
repo, baseBranch, headBranch := ctx.Repo.Repository, ctx.Repo.BranchName, parsed.TargetBranchName
|
repo, baseBranch, headBranch := ctx.Repo.Repository, parsed.OldBranchName, parsed.NewBranchName
|
||||||
if repo.UnitEnabled(ctx, unit.TypePullRequests) {
|
if ctx.Repo.Repository.IsFork && parsed.CommitFormOptions.CanCreateBasePullRequest {
|
||||||
redirectToPullRequest = true
|
|
||||||
} else if parsed.CommitFormBehaviors.CanCreateBasePullRequest {
|
|
||||||
redirectToPullRequest = true
|
redirectToPullRequest = true
|
||||||
baseBranch = repo.BaseRepo.DefaultBranch
|
baseBranch = repo.BaseRepo.DefaultBranch
|
||||||
headBranch = repo.Owner.Name + "/" + repo.Name + ":" + headBranch
|
headBranch = repo.Owner.Name + "/" + repo.Name + ":" + headBranch
|
||||||
repo = repo.BaseRepo
|
repo = repo.BaseRepo
|
||||||
|
} else if repo.UnitEnabled(ctx, unit.TypePullRequests) {
|
||||||
|
redirectToPullRequest = true
|
||||||
}
|
}
|
||||||
if redirectToPullRequest {
|
if redirectToPullRequest {
|
||||||
ctx.JSONRedirect(repo.Link() + "/compare/" + util.PathEscapeSegments(baseBranch) + "..." + util.PathEscapeSegments(headBranch))
|
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")
|
// redirect to the newly updated file
|
||||||
if returnURI == "" || !httplib.IsCurrentGiteaSiteURL(ctx, returnURI) {
|
redirectTo := util.URLJoin(ctx.Repo.RepoLink, "src/branch", util.PathEscapeSegments(parsed.NewBranchName), util.PathEscapeSegments(treePath))
|
||||||
returnURI = util.URLJoin(ctx.Repo.RepoLink, "src/branch", util.PathEscapeSegments(parsed.TargetBranchName), util.PathEscapeSegments(treePath))
|
ctx.JSONRedirect(redirectTo)
|
||||||
}
|
|
||||||
ctx.JSONRedirect(returnURI)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func editFileOpenExisting(ctx *context.Context) (prefetch []byte, dataRc io.ReadCloser, fInfo *fileInfo) {
|
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) {
|
func EditFilePost(ctx *context.Context) {
|
||||||
editorAction := ctx.PathParam("editor_action")
|
editorAction := ctx.PathParam("editor_action")
|
||||||
isNewFile := editorAction == "_new"
|
isNewFile := editorAction == "_new"
|
||||||
parsed := parseEditorCommitSubmittedForm[*forms.EditRepoFileForm](ctx)
|
parsed := prepareEditorCommitSubmittedForm[*forms.EditRepoFileForm](ctx)
|
||||||
if ctx.Written() {
|
if ctx.Written() {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
@ -292,8 +345,8 @@ func EditFilePost(ctx *context.Context) {
|
||||||
|
|
||||||
_, err := files_service.ChangeRepoFiles(ctx, ctx.Repo.Repository, ctx.Doer, &files_service.ChangeRepoFilesOptions{
|
_, err := files_service.ChangeRepoFiles(ctx, ctx.Repo.Repository, ctx.Doer, &files_service.ChangeRepoFilesOptions{
|
||||||
LastCommitID: parsed.form.LastCommit,
|
LastCommitID: parsed.form.LastCommit,
|
||||||
OldBranch: ctx.Repo.BranchName,
|
OldBranch: parsed.OldBranchName,
|
||||||
NewBranch: parsed.TargetBranchName,
|
NewBranch: parsed.NewBranchName,
|
||||||
Message: parsed.GetCommitMessage(defaultCommitMessage),
|
Message: parsed.GetCommitMessage(defaultCommitMessage),
|
||||||
Files: []*files_service.ChangeRepoFile{
|
Files: []*files_service.ChangeRepoFile{
|
||||||
{
|
{
|
||||||
|
|
@ -308,7 +361,7 @@ func EditFilePost(ctx *context.Context) {
|
||||||
Committer: parsed.GitCommitter,
|
Committer: parsed.GitCommitter,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
editorHandleFileOperationError(ctx, parsed.TargetBranchName, err)
|
editorHandleFileOperationError(ctx, parsed.NewBranchName, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -327,7 +380,7 @@ func DeleteFile(ctx *context.Context) {
|
||||||
|
|
||||||
// DeleteFilePost response for deleting file
|
// DeleteFilePost response for deleting file
|
||||||
func DeleteFilePost(ctx *context.Context) {
|
func DeleteFilePost(ctx *context.Context) {
|
||||||
parsed := parseEditorCommitSubmittedForm[*forms.DeleteRepoFileForm](ctx)
|
parsed := prepareEditorCommitSubmittedForm[*forms.DeleteRepoFileForm](ctx)
|
||||||
if ctx.Written() {
|
if ctx.Written() {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
@ -335,8 +388,8 @@ func DeleteFilePost(ctx *context.Context) {
|
||||||
treePath := ctx.Repo.TreePath
|
treePath := ctx.Repo.TreePath
|
||||||
_, err := files_service.ChangeRepoFiles(ctx, ctx.Repo.Repository, ctx.Doer, &files_service.ChangeRepoFilesOptions{
|
_, err := files_service.ChangeRepoFiles(ctx, ctx.Repo.Repository, ctx.Doer, &files_service.ChangeRepoFilesOptions{
|
||||||
LastCommitID: parsed.form.LastCommit,
|
LastCommitID: parsed.form.LastCommit,
|
||||||
OldBranch: ctx.Repo.BranchName,
|
OldBranch: parsed.OldBranchName,
|
||||||
NewBranch: parsed.TargetBranchName,
|
NewBranch: parsed.NewBranchName,
|
||||||
Files: []*files_service.ChangeRepoFile{
|
Files: []*files_service.ChangeRepoFile{
|
||||||
{
|
{
|
||||||
Operation: "delete",
|
Operation: "delete",
|
||||||
|
|
@ -349,29 +402,29 @@ func DeleteFilePost(ctx *context.Context) {
|
||||||
Committer: parsed.GitCommitter,
|
Committer: parsed.GitCommitter,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
editorHandleFileOperationError(ctx, parsed.TargetBranchName, err)
|
editorHandleFileOperationError(ctx, parsed.NewBranchName, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx.Flash.Success(ctx.Tr("repo.editor.file_delete_success", treePath))
|
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)
|
redirectForCommitChoice(ctx, parsed, redirectTreePath)
|
||||||
}
|
}
|
||||||
|
|
||||||
func UploadFile(ctx *context.Context) {
|
func UploadFile(ctx *context.Context) {
|
||||||
ctx.Data["PageIsUpload"] = true
|
ctx.Data["PageIsUpload"] = true
|
||||||
upload.AddUploadContext(ctx, "repo")
|
|
||||||
prepareTreePathFieldsAndPaths(ctx, ctx.Repo.TreePath)
|
prepareTreePathFieldsAndPaths(ctx, ctx.Repo.TreePath)
|
||||||
|
opts := prepareEditorCommitFormOptions(ctx, "_upload")
|
||||||
prepareEditorCommitFormOptions(ctx, "_upload")
|
|
||||||
if ctx.Written() {
|
if ctx.Written() {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
upload.AddUploadContextForRepo(ctx, opts.TargetRepo)
|
||||||
|
|
||||||
ctx.HTML(http.StatusOK, tplUploadFile)
|
ctx.HTML(http.StatusOK, tplUploadFile)
|
||||||
}
|
}
|
||||||
|
|
||||||
func UploadFilePost(ctx *context.Context) {
|
func UploadFilePost(ctx *context.Context) {
|
||||||
parsed := parseEditorCommitSubmittedForm[*forms.UploadRepoFileForm](ctx)
|
parsed := prepareEditorCommitSubmittedForm[*forms.UploadRepoFileForm](ctx)
|
||||||
if ctx.Written() {
|
if ctx.Written() {
|
||||||
return
|
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, "/"))
|
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{
|
err := files_service.UploadRepoFiles(ctx, ctx.Repo.Repository, ctx.Doer, &files_service.UploadRepoFileOptions{
|
||||||
LastCommitID: parsed.form.LastCommit,
|
LastCommitID: parsed.form.LastCommit,
|
||||||
OldBranch: ctx.Repo.BranchName,
|
OldBranch: parsed.OldBranchName,
|
||||||
NewBranch: parsed.TargetBranchName,
|
NewBranch: parsed.NewBranchName,
|
||||||
TreePath: parsed.form.TreePath,
|
TreePath: parsed.form.TreePath,
|
||||||
Message: parsed.GetCommitMessage(defaultCommitMessage),
|
Message: parsed.GetCommitMessage(defaultCommitMessage),
|
||||||
Files: parsed.form.Files,
|
Files: parsed.form.Files,
|
||||||
|
|
@ -389,7 +442,7 @@ func UploadFilePost(ctx *context.Context) {
|
||||||
Committer: parsed.GitCommitter,
|
Committer: parsed.GitCommitter,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
editorHandleFileOperationError(ctx, parsed.TargetBranchName, err)
|
editorHandleFileOperationError(ctx, parsed.NewBranchName, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
redirectForCommitChoice(ctx, parsed, parsed.form.TreePath)
|
redirectForCommitChoice(ctx, parsed, parsed.form.TreePath)
|
||||||
|
|
|
||||||
|
|
@ -25,7 +25,7 @@ func NewDiffPatch(ctx *context.Context) {
|
||||||
|
|
||||||
// NewDiffPatchPost response for sending patch page
|
// NewDiffPatchPost response for sending patch page
|
||||||
func NewDiffPatchPost(ctx *context.Context) {
|
func NewDiffPatchPost(ctx *context.Context) {
|
||||||
parsed := parseEditorCommitSubmittedForm[*forms.EditRepoFileForm](ctx)
|
parsed := prepareEditorCommitSubmittedForm[*forms.EditRepoFileForm](ctx)
|
||||||
if ctx.Written() {
|
if ctx.Written() {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
@ -33,8 +33,8 @@ func NewDiffPatchPost(ctx *context.Context) {
|
||||||
defaultCommitMessage := ctx.Locale.TrString("repo.editor.patch")
|
defaultCommitMessage := ctx.Locale.TrString("repo.editor.patch")
|
||||||
_, err := files.ApplyDiffPatch(ctx, ctx.Repo.Repository, ctx.Doer, &files.ApplyDiffPatchOptions{
|
_, err := files.ApplyDiffPatch(ctx, ctx.Repo.Repository, ctx.Doer, &files.ApplyDiffPatchOptions{
|
||||||
LastCommitID: parsed.form.LastCommit,
|
LastCommitID: parsed.form.LastCommit,
|
||||||
OldBranch: ctx.Repo.BranchName,
|
OldBranch: parsed.OldBranchName,
|
||||||
NewBranch: parsed.TargetBranchName,
|
NewBranch: parsed.NewBranchName,
|
||||||
Message: parsed.GetCommitMessage(defaultCommitMessage),
|
Message: parsed.GetCommitMessage(defaultCommitMessage),
|
||||||
Content: strings.ReplaceAll(parsed.form.Content.Value(), "\r\n", "\n"),
|
Content: strings.ReplaceAll(parsed.form.Content.Value(), "\r\n", "\n"),
|
||||||
Author: parsed.GitCommitter,
|
Author: parsed.GitCommitter,
|
||||||
|
|
@ -44,7 +44,7 @@ func NewDiffPatchPost(ctx *context.Context) {
|
||||||
err = util.ErrorWrapLocale(err, "repo.editor.fail_to_apply_patch")
|
err = util.ErrorWrapLocale(err, "repo.editor.fail_to_apply_patch")
|
||||||
}
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
editorHandleFileOperationError(ctx, parsed.TargetBranchName, err)
|
editorHandleFileOperationError(ctx, parsed.NewBranchName, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
redirectForCommitChoice(ctx, parsed, parsed.form.TreePath)
|
redirectForCommitChoice(ctx, parsed, parsed.form.TreePath)
|
||||||
|
|
|
||||||
|
|
@ -45,7 +45,7 @@ func CherryPick(ctx *context.Context) {
|
||||||
|
|
||||||
func CherryPickPost(ctx *context.Context) {
|
func CherryPickPost(ctx *context.Context) {
|
||||||
fromCommitID := ctx.PathParam("sha")
|
fromCommitID := ctx.PathParam("sha")
|
||||||
parsed := parseEditorCommitSubmittedForm[*forms.CherryPickForm](ctx)
|
parsed := prepareEditorCommitSubmittedForm[*forms.CherryPickForm](ctx)
|
||||||
if ctx.Written() {
|
if ctx.Written() {
|
||||||
return
|
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))
|
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{
|
opts := &files.ApplyDiffPatchOptions{
|
||||||
LastCommitID: parsed.form.LastCommit,
|
LastCommitID: parsed.form.LastCommit,
|
||||||
OldBranch: ctx.Repo.BranchName,
|
OldBranch: parsed.OldBranchName,
|
||||||
NewBranch: parsed.TargetBranchName,
|
NewBranch: parsed.NewBranchName,
|
||||||
Message: parsed.GetCommitMessage(defaultCommitMessage),
|
Message: parsed.GetCommitMessage(defaultCommitMessage),
|
||||||
Author: parsed.GitCommitter,
|
Author: parsed.GitCommitter,
|
||||||
Committer: parsed.GitCommitter,
|
Committer: parsed.GitCommitter,
|
||||||
|
|
@ -78,7 +78,7 @@ func CherryPickPost(ctx *context.Context) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
editorHandleFileOperationError(ctx, parsed.TargetBranchName, err)
|
editorHandleFileOperationError(ctx, parsed.NewBranchName, err)
|
||||||
return
|
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"
|
git_model "code.gitea.io/gitea/models/git"
|
||||||
repo_model "code.gitea.io/gitea/models/repo"
|
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/git"
|
||||||
"code.gitea.io/gitea/modules/json"
|
"code.gitea.io/gitea/modules/json"
|
||||||
"code.gitea.io/gitea/modules/log"
|
"code.gitea.io/gitea/modules/log"
|
||||||
|
repo_module "code.gitea.io/gitea/modules/repository"
|
||||||
context_service "code.gitea.io/gitea/services/context"
|
context_service "code.gitea.io/gitea/services/context"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -83,3 +85,26 @@ func getParentTreeFields(treePath string) (treeNames, treePaths []string) {
|
||||||
}
|
}
|
||||||
return treeNames, treePaths
|
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,
|
BaseRepo: forkRepo,
|
||||||
Name: form.RepoName,
|
Name: form.RepoName,
|
||||||
Description: form.Description,
|
Description: form.Description,
|
||||||
SingleBranch: form.ForkSingleBranch,
|
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 {
|
if err != nil {
|
||||||
ctx.Data["Err_RepoName"] = true
|
ctx.Data["Err_RepoName"] = true
|
||||||
switch {
|
switch {
|
||||||
case repo_model.IsErrReachLimitOfRepo(err):
|
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)
|
msg := ctx.TrN(maxCreationLimit, "repo.form.reach_limit_of_creation_1", "repo.form.reach_limit_of_creation_n", maxCreationLimit)
|
||||||
ctx.JSONError(msg)
|
ctx.JSONError(msg)
|
||||||
case repo_model.IsErrRepoAlreadyExist(err):
|
case repo_model.IsErrRepoAlreadyExist(err):
|
||||||
|
|
@ -224,9 +232,7 @@ func ForkPost(ctx *context.Context) {
|
||||||
default:
|
default:
|
||||||
ctx.ServerError("ForkPost", err)
|
ctx.ServerError("ForkPost", err)
|
||||||
}
|
}
|
||||||
return
|
return nil
|
||||||
}
|
}
|
||||||
|
return repo
|
||||||
log.Trace("Repository forked[%d]: %s/%s", forkRepo.ID, ctxUser.Name, repo.Name)
|
|
||||||
ctx.JSONRedirect(ctxUser.HomeLink() + "/" + url.PathEscape(repo.Name))
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -290,7 +290,7 @@ func prepareToRenderFile(ctx *context.Context, entry *git.TreeEntry) {
|
||||||
|
|
||||||
func prepareToRenderButtons(ctx *context.Context, lfsLock *git_model.LFSLock) {
|
func prepareToRenderButtons(ctx *context.Context, lfsLock *git_model.LFSLock) {
|
||||||
// archived or mirror repository, the buttons should not be shown
|
// 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
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -302,7 +302,9 @@ func prepareToRenderButtons(ctx *context.Context, lfsLock *git_model.LFSLock) {
|
||||||
}
|
}
|
||||||
|
|
||||||
if !ctx.Repo.CanWriteToBranch(ctx, ctx.Doer, ctx.Repo.BranchName) {
|
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["EditFileTooltip"] = ctx.Tr("repo.editor.fork_before_edit")
|
||||||
|
ctx.Data["CanDeleteFile"] = true
|
||||||
ctx.Data["DeleteFileTooltip"] = ctx.Tr("repo.editor.must_have_write_access")
|
ctx.Data["DeleteFileTooltip"] = ctx.Tr("repo.editor.must_have_write_access")
|
||||||
return
|
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)
|
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
|
ctx.Data["CanEditReadmeFile"] = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ package repo
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
gocontext "context"
|
gocontext "context"
|
||||||
|
"html/template"
|
||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
|
|
@ -61,9 +62,9 @@ func MustEnableWiki(ctx *context.Context) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
unit, err := ctx.Repo.Repository.GetUnit(ctx, unit.TypeExternalWiki)
|
repoUnit, err := ctx.Repo.Repository.GetUnit(ctx, unit.TypeExternalWiki)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
ctx.Redirect(unit.ExternalWikiConfig().ExternalWikiURL)
|
ctx.Redirect(repoUnit.ExternalWikiConfig().ExternalWikiURL)
|
||||||
return
|
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) {
|
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 {
|
if errGitRepo != nil {
|
||||||
ctx.ServerError("OpenRepository", errGitRepo)
|
ctx.ServerError("OpenRepository", errGitRepo)
|
||||||
return nil, nil, 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) {
|
func renderViewPage(ctx *context.Context) (*git.Repository, *git.TreeEntry) {
|
||||||
wikiRepo, commit, err := findWikiRepoCommit(ctx)
|
wikiGitRepo, commit, err := findWikiRepoCommit(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if wikiRepo != nil {
|
|
||||||
wikiRepo.Close()
|
|
||||||
}
|
|
||||||
if !git.IsErrNotExist(err) {
|
if !git.IsErrNotExist(err) {
|
||||||
ctx.ServerError("GetBranchCommit", err)
|
ctx.ServerError("GetBranchCommit", err)
|
||||||
}
|
}
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get page list.
|
// get the wiki pages list.
|
||||||
entries, err := commit.ListEntries()
|
entries, err := commit.ListEntries()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if wikiRepo != nil {
|
|
||||||
wikiRepo.Close()
|
|
||||||
}
|
|
||||||
ctx.ServerError("ListEntries", err)
|
ctx.ServerError("ListEntries", err)
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
|
@ -208,9 +203,6 @@ func renderViewPage(ctx *context.Context) (*git.Repository, *git.TreeEntry) {
|
||||||
if repo_model.IsErrWikiInvalidFileName(err) {
|
if repo_model.IsErrWikiInvalidFileName(err) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if wikiRepo != nil {
|
|
||||||
wikiRepo.Close()
|
|
||||||
}
|
|
||||||
ctx.ServerError("WikiFilenameToName", err)
|
ctx.ServerError("WikiFilenameToName", err)
|
||||||
return nil, nil
|
return nil, nil
|
||||||
} else if wikiName == "_Sidebar" || wikiName == "_Footer" {
|
} 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)))
|
ctx.Redirect(util.URLJoin(ctx.Repo.RepoLink, "wiki/raw", string(pageName)))
|
||||||
}
|
}
|
||||||
if entry == nil || ctx.Written() {
|
if entry == nil || ctx.Written() {
|
||||||
if wikiRepo != nil {
|
|
||||||
wikiRepo.Close()
|
|
||||||
}
|
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// get filecontent
|
// get page content
|
||||||
data := wikiContentsByEntry(ctx, entry)
|
data := wikiContentsByEntry(ctx, entry)
|
||||||
if ctx.Written() {
|
if ctx.Written() {
|
||||||
if wikiRepo != nil {
|
|
||||||
wikiRepo.Close()
|
|
||||||
}
|
|
||||||
return nil, nil
|
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)
|
rctx := renderhelper.NewRenderContextRepoWiki(ctx, ctx.Repo.Repository)
|
||||||
|
|
||||||
buf := &strings.Builder{}
|
renderFn := func(data []byte) (escaped *charset.EscapeStatus, output template.HTML, err error) {
|
||||||
renderFn := func(data []byte) (escaped *charset.EscapeStatus, output string, err error) {
|
buf := &strings.Builder{}
|
||||||
markupRd, markupWr := io.Pipe()
|
markupRd, markupWr := io.Pipe()
|
||||||
defer markupWr.Close()
|
defer markupWr.Close()
|
||||||
done := make(chan struct{})
|
done := make(chan struct{})
|
||||||
go func() {
|
go func() {
|
||||||
// We allow NBSP here this is rendered
|
// We allow NBSP here this is rendered
|
||||||
escaped, _ = charset.EscapeControlReader(markupRd, buf, ctx.Locale, charset.RuneNBSP)
|
escaped, _ = charset.EscapeControlReader(markupRd, buf, ctx.Locale, charset.RuneNBSP)
|
||||||
output = buf.String()
|
output = template.HTML(buf.String())
|
||||||
buf.Reset()
|
buf.Reset()
|
||||||
close(done)
|
close(done)
|
||||||
}()
|
}()
|
||||||
|
|
@ -311,75 +271,61 @@ func renderViewPage(ctx *context.Context) (*git.Repository, *git.TreeEntry) {
|
||||||
return escaped, output, err
|
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 err != nil {
|
||||||
if wikiRepo != nil {
|
|
||||||
wikiRepo.Close()
|
|
||||||
}
|
|
||||||
ctx.ServerError("Render", err)
|
ctx.ServerError("Render", err)
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
if rctx.SidebarTocNode != nil {
|
if rctx.SidebarTocNode != nil {
|
||||||
sb := &strings.Builder{}
|
sb := strings.Builder{}
|
||||||
err = markdown.SpecializedMarkdown(rctx).Renderer().Render(sb, nil, rctx.SidebarTocNode)
|
if err = markdown.SpecializedMarkdown(rctx).Renderer().Render(&sb, nil, rctx.SidebarTocNode); err != nil {
|
||||||
if err != nil {
|
|
||||||
log.Error("Failed to render wiki sidebar TOC: %v", err)
|
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 {
|
if !isSideBar {
|
||||||
buf.Reset()
|
sidebarContent, _, _, _ := wikiContentsByName(ctx, commit, "_Sidebar")
|
||||||
ctx.Data["sidebarEscapeStatus"], ctx.Data["sidebarContent"], err = renderFn(sidebarContent)
|
if ctx.Written() {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
ctx.Data["WikiSidebarEscapeStatus"], ctx.Data["WikiSidebarHTML"], err = renderFn(sidebarContent)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if wikiRepo != nil {
|
|
||||||
wikiRepo.Close()
|
|
||||||
}
|
|
||||||
ctx.ServerError("Render", err)
|
ctx.ServerError("Render", err)
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
ctx.Data["sidebarPresent"] = sidebarContent != nil
|
|
||||||
} else {
|
|
||||||
ctx.Data["sidebarPresent"] = false
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if !isFooter {
|
if !isFooter {
|
||||||
buf.Reset()
|
footerContent, _, _, _ := wikiContentsByName(ctx, commit, "_Footer")
|
||||||
ctx.Data["footerEscapeStatus"], ctx.Data["footerContent"], err = renderFn(footerContent)
|
if ctx.Written() {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
ctx.Data["WikiFooterEscapeStatus"], ctx.Data["WikiFooterHTML"], err = renderFn(footerContent)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if wikiRepo != nil {
|
|
||||||
wikiRepo.Close()
|
|
||||||
}
|
|
||||||
ctx.ServerError("Render", err)
|
ctx.ServerError("Render", err)
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
ctx.Data["footerPresent"] = footerContent != nil
|
|
||||||
} else {
|
|
||||||
ctx.Data["footerPresent"] = false
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// get commit count - wiki revisions
|
// 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
|
ctx.Data["CommitCount"] = commitsCount
|
||||||
|
|
||||||
return wikiRepo, entry
|
return wikiGitRepo, entry
|
||||||
}
|
}
|
||||||
|
|
||||||
func renderRevisionPage(ctx *context.Context) (*git.Repository, *git.TreeEntry) {
|
func renderRevisionPage(ctx *context.Context) (*git.Repository, *git.TreeEntry) {
|
||||||
wikiRepo, commit, err := findWikiRepoCommit(ctx)
|
wikiGitRepo, commit, err := findWikiRepoCommit(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if wikiRepo != nil {
|
|
||||||
wikiRepo.Close()
|
|
||||||
}
|
|
||||||
if !git.IsErrNotExist(err) {
|
if !git.IsErrNotExist(err) {
|
||||||
ctx.ServerError("GetBranchCommit", err)
|
ctx.ServerError("GetBranchCommit", err)
|
||||||
}
|
}
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// get requested pagename
|
// get requested page name
|
||||||
pageName := wiki_service.WebPathFromRequest(ctx.PathParamRaw("*"))
|
pageName := wiki_service.WebPathFromRequest(ctx.PathParamRaw("*"))
|
||||||
if len(pageName) == 0 {
|
if len(pageName) == 0 {
|
||||||
pageName = "Home"
|
pageName = "Home"
|
||||||
|
|
@ -394,50 +340,35 @@ func renderRevisionPage(ctx *context.Context) (*git.Repository, *git.TreeEntry)
|
||||||
ctx.Data["Username"] = ctx.Repo.Owner.Name
|
ctx.Data["Username"] = ctx.Repo.Owner.Name
|
||||||
ctx.Data["Reponame"] = ctx.Repo.Repository.Name
|
ctx.Data["Reponame"] = ctx.Repo.Repository.Name
|
||||||
|
|
||||||
// lookup filename in wiki - get filecontent, gitTree entry , real filename
|
// lookup filename in wiki - get page content, gitTree entry , real filename
|
||||||
data, entry, pageFilename, noEntry := wikiContentsByName(ctx, commit, pageName)
|
_, entry, pageFilename, noEntry := wikiContentsByName(ctx, commit, pageName)
|
||||||
if noEntry {
|
if noEntry {
|
||||||
ctx.Redirect(ctx.Repo.RepoLink + "/wiki/?action=_pages")
|
ctx.Redirect(ctx.Repo.RepoLink + "/wiki/?action=_pages")
|
||||||
}
|
}
|
||||||
if entry == nil || ctx.Written() {
|
if entry == nil || ctx.Written() {
|
||||||
if wikiRepo != nil {
|
|
||||||
wikiRepo.Close()
|
|
||||||
}
|
|
||||||
return nil, nil
|
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
|
// 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
|
ctx.Data["CommitCount"] = commitsCount
|
||||||
|
|
||||||
// get page
|
// get page
|
||||||
page := max(ctx.FormInt("page"), 1)
|
page := max(ctx.FormInt("page"), 1)
|
||||||
|
|
||||||
// get Commit Count
|
// get Commit Count
|
||||||
commitsHistory, err := wikiRepo.CommitsByFileAndRange(
|
commitsHistory, err := wikiGitRepo.CommitsByFileAndRange(
|
||||||
git.CommitsByFileAndRangeOptions{
|
git.CommitsByFileAndRangeOptions{
|
||||||
Revision: ctx.Repo.Repository.DefaultWikiBranch,
|
Revision: ctx.Repo.Repository.DefaultWikiBranch,
|
||||||
File: pageFilename,
|
File: pageFilename,
|
||||||
Page: page,
|
Page: page,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if wikiRepo != nil {
|
|
||||||
wikiRepo.Close()
|
|
||||||
}
|
|
||||||
ctx.ServerError("CommitsByFileAndRange", err)
|
ctx.ServerError("CommitsByFileAndRange", err)
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
ctx.Data["Commits"], err = git_service.ConvertFromGitCommit(ctx, commitsHistory, ctx.Repo.Repository)
|
ctx.Data["Commits"], err = git_service.ConvertFromGitCommit(ctx, commitsHistory, ctx.Repo.Repository)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if wikiRepo != nil {
|
|
||||||
wikiRepo.Close()
|
|
||||||
}
|
|
||||||
ctx.ServerError("ConvertFromGitCommit", err)
|
ctx.ServerError("ConvertFromGitCommit", err)
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
|
@ -446,16 +377,11 @@ func renderRevisionPage(ctx *context.Context) (*git.Repository, *git.TreeEntry)
|
||||||
pager.AddParamFromRequest(ctx.Req)
|
pager.AddParamFromRequest(ctx.Req)
|
||||||
ctx.Data["Page"] = pager
|
ctx.Data["Page"] = pager
|
||||||
|
|
||||||
return wikiRepo, entry
|
return wikiGitRepo, entry
|
||||||
}
|
}
|
||||||
|
|
||||||
func renderEditPage(ctx *context.Context) {
|
func renderEditPage(ctx *context.Context) {
|
||||||
wikiRepo, commit, err := findWikiRepoCommit(ctx)
|
_, commit, err := findWikiRepoCommit(ctx)
|
||||||
defer func() {
|
|
||||||
if wikiRepo != nil {
|
|
||||||
_ = wikiRepo.Close()
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if !git.IsErrNotExist(err) {
|
if !git.IsErrNotExist(err) {
|
||||||
ctx.ServerError("GetBranchCommit", err)
|
ctx.ServerError("GetBranchCommit", err)
|
||||||
|
|
@ -463,7 +389,7 @@ func renderEditPage(ctx *context.Context) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// get requested pagename
|
// get requested page name
|
||||||
pageName := wiki_service.WebPathFromRequest(ctx.PathParamRaw("*"))
|
pageName := wiki_service.WebPathFromRequest(ctx.PathParamRaw("*"))
|
||||||
if len(pageName) == 0 {
|
if len(pageName) == 0 {
|
||||||
pageName = "Home"
|
pageName = "Home"
|
||||||
|
|
@ -487,17 +413,13 @@ func renderEditPage(ctx *context.Context) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// get filecontent
|
// get wiki page content
|
||||||
data := wikiContentsByEntry(ctx, entry)
|
data := wikiContentsByEntry(ctx, entry)
|
||||||
if ctx.Written() {
|
if ctx.Written() {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx.Data["content"] = string(data)
|
ctx.Data["WikiEditContent"] = string(data)
|
||||||
ctx.Data["sidebarPresent"] = false
|
|
||||||
ctx.Data["sidebarContent"] = ""
|
|
||||||
ctx.Data["footerPresent"] = false
|
|
||||||
ctx.Data["footerContent"] = ""
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// WikiPost renders post of wiki page
|
// WikiPost renders post of wiki page
|
||||||
|
|
@ -559,12 +481,7 @@ func Wiki(ctx *context.Context) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
wikiRepo, entry := renderViewPage(ctx)
|
wikiGitRepo, entry := renderViewPage(ctx)
|
||||||
defer func() {
|
|
||||||
if wikiRepo != nil {
|
|
||||||
wikiRepo.Close()
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
if ctx.Written() {
|
if ctx.Written() {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
@ -580,7 +497,7 @@ func Wiki(ctx *context.Context) {
|
||||||
ctx.Data["FormatWarning"] = ext + " rendering is not supported at the moment. Rendered as Markdown."
|
ctx.Data["FormatWarning"] = ext + " rendering is not supported at the moment. Rendered as Markdown."
|
||||||
}
|
}
|
||||||
// Get last change information.
|
// Get last change information.
|
||||||
lastCommit, err := wikiRepo.GetCommitByPath(wikiPath)
|
lastCommit, err := wikiGitRepo.GetCommitByPath(wikiPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ctx.ServerError("GetCommitByPath", err)
|
ctx.ServerError("GetCommitByPath", err)
|
||||||
return
|
return
|
||||||
|
|
@ -600,13 +517,7 @@ func WikiRevision(ctx *context.Context) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
wikiRepo, entry := renderRevisionPage(ctx)
|
wikiGitRepo, entry := renderRevisionPage(ctx)
|
||||||
defer func() {
|
|
||||||
if wikiRepo != nil {
|
|
||||||
wikiRepo.Close()
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
if ctx.Written() {
|
if ctx.Written() {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
@ -618,7 +529,7 @@ func WikiRevision(ctx *context.Context) {
|
||||||
|
|
||||||
// Get last change information.
|
// Get last change information.
|
||||||
wikiPath := entry.Name()
|
wikiPath := entry.Name()
|
||||||
lastCommit, err := wikiRepo.GetCommitByPath(wikiPath)
|
lastCommit, err := wikiGitRepo.GetCommitByPath(wikiPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ctx.ServerError("GetCommitByPath", err)
|
ctx.ServerError("GetCommitByPath", err)
|
||||||
return
|
return
|
||||||
|
|
@ -638,12 +549,7 @@ func WikiPages(ctx *context.Context) {
|
||||||
ctx.Data["Title"] = ctx.Tr("repo.wiki.pages")
|
ctx.Data["Title"] = ctx.Tr("repo.wiki.pages")
|
||||||
ctx.Data["CanWriteWiki"] = ctx.Repo.CanWrite(unit.TypeWiki) && !ctx.Repo.Repository.IsArchived
|
ctx.Data["CanWriteWiki"] = ctx.Repo.CanWrite(unit.TypeWiki) && !ctx.Repo.Repository.IsArchived
|
||||||
|
|
||||||
wikiRepo, commit, err := findWikiRepoCommit(ctx)
|
_, commit, err := findWikiRepoCommit(ctx)
|
||||||
defer func() {
|
|
||||||
if wikiRepo != nil {
|
|
||||||
_ = wikiRepo.Close()
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ctx.Redirect(ctx.Repo.RepoLink + "/wiki")
|
ctx.Redirect(ctx.Repo.RepoLink + "/wiki")
|
||||||
return
|
return
|
||||||
|
|
@ -697,13 +603,7 @@ func WikiPages(ctx *context.Context) {
|
||||||
|
|
||||||
// WikiRaw outputs raw blob requested by user (image for example)
|
// WikiRaw outputs raw blob requested by user (image for example)
|
||||||
func WikiRaw(ctx *context.Context) {
|
func WikiRaw(ctx *context.Context) {
|
||||||
wikiRepo, commit, err := findWikiRepoCommit(ctx)
|
_, commit, err := findWikiRepoCommit(ctx)
|
||||||
defer func() {
|
|
||||||
if wikiRepo != nil {
|
|
||||||
wikiRepo.Close()
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if git.IsErrNotExist(err) {
|
if git.IsErrNotExist(err) {
|
||||||
ctx.NotFound(nil)
|
ctx.NotFound(nil)
|
||||||
|
|
|
||||||
|
|
@ -164,7 +164,7 @@ func TestEditWiki(t *testing.T) {
|
||||||
EditWiki(ctx)
|
EditWiki(ctx)
|
||||||
assert.Equal(t, http.StatusOK, ctx.Resp.WrittenStatus())
|
assert.Equal(t, http.StatusOK, ctx.Resp.WrittenStatus())
|
||||||
assert.EqualValues(t, "Home", ctx.Data["Title"])
|
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, _ = contexttest.MockContext(t, "user2/repo1/wiki/jpeg.jpg?action=_edit")
|
||||||
ctx.SetPathParam("*", "jpeg.jpg")
|
ctx.SetPathParam("*", "jpeg.jpg")
|
||||||
|
|
|
||||||
|
|
@ -1313,23 +1313,35 @@ func registerWebRoutes(m *web.Router) {
|
||||||
}, reqSignIn, context.RepoAssignment, context.RepoMustNotBeArchived())
|
}, reqSignIn, context.RepoAssignment, context.RepoMustNotBeArchived())
|
||||||
// end "/{username}/{reponame}": create or edit issues, pulls, labels, milestones
|
// 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.Group("", func() {
|
m.Group("", func() {
|
||||||
m.Post("/_preview/*", repo.DiffPreviewPost)
|
// "GET" requests only need "code reader" permission, "POST" requests need "code writer" permission.
|
||||||
m.Combo("/{editor_action:_edit}/*").Get(repo.EditFile).
|
// Because reader can "fork and edit"
|
||||||
Post(web.Bind(forms.EditRepoFileForm{}), repo.EditFilePost)
|
canWriteToBranch := context.CanWriteToBranch()
|
||||||
m.Combo("/{editor_action:_new}/*").Get(repo.EditFile).
|
m.Post("/_preview/*", repo.DiffPreviewPost) // read-only, fine with "code reader"
|
||||||
Post(web.Bind(forms.EditRepoFileForm{}), repo.EditFilePost)
|
m.Post("/_fork/*", repo.ForkToEditPost) // read-only, fork to own repo, fine with "code reader"
|
||||||
m.Combo("/_delete/*").Get(repo.DeleteFile).
|
|
||||||
Post(web.Bind(forms.DeleteRepoFileForm{}), repo.DeleteFilePost)
|
// the path params are used in PrepareCommitFormOptions to construct the correct form action URL
|
||||||
m.Combo("/_upload/*", repo.MustBeAbleToUpload).Get(repo.UploadFile).
|
m.Combo("/{editor_action:_edit}/*").
|
||||||
Post(web.Bind(forms.UploadRepoFileForm{}), repo.UploadFilePost)
|
Get(repo.EditFile).
|
||||||
m.Combo("/_diffpatch/*").Get(repo.NewDiffPatch).
|
Post(web.Bind(forms.EditRepoFileForm{}), canWriteToBranch, repo.EditFilePost)
|
||||||
Post(web.Bind(forms.EditRepoFileForm{}), repo.NewDiffPatchPost)
|
m.Combo("/{editor_action:_new}/*").
|
||||||
m.Combo("/_cherrypick/{sha:([a-f0-9]{7,64})}/*").Get(repo.CherryPick).
|
Get(repo.EditFile).
|
||||||
Post(web.Bind(forms.CherryPickForm{}), repo.CherryPickPost)
|
Post(web.Bind(forms.EditRepoFileForm{}), canWriteToBranch, repo.EditFilePost)
|
||||||
}, context.RepoRefByType(git.RefTypeBranch), context.CanWriteToBranch(), repo.WebGitOperationCommonData)
|
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.Group("", func() {
|
||||||
m.Post("/upload-file", repo.UploadFileToServer)
|
m.Post("/upload-file", repo.UploadFileToServer)
|
||||||
m.Post("/upload-remove", repo.RemoveUploadFileFromServer)
|
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)
|
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.
|
// CanCreateBranch returns true if repository is editable and user has proper access level.
|
||||||
func (r *Repository) CanCreateBranch() bool {
|
func (r *Repository) CanCreateBranch() bool {
|
||||||
return r.Permission.CanWrite(unit_model.TypeCode) && r.Repository.CanCreateBranch()
|
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
|
CanCommitToBranch bool
|
||||||
EditorEnabled bool
|
|
||||||
UserCanPush bool
|
UserCanPush bool
|
||||||
RequireSigned bool
|
RequireSigned bool
|
||||||
WillSign bool
|
WillSign bool
|
||||||
|
|
@ -106,51 +105,84 @@ type CommitFormBehaviors struct {
|
||||||
CanCreateBasePullRequest bool
|
CanCreateBasePullRequest bool
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *Repository) PrepareCommitFormBehaviors(ctx *Context, doer *user_model.User) (*CommitFormBehaviors, error) {
|
func PrepareCommitFormOptions(ctx *Context, doer *user_model.User, targetRepo *repo_model.Repository, doerRepoPerm access_model.Permission, refName git.RefName) (*CommitFormOptions, error) {
|
||||||
protectedBranch, err := git_model.GetFirstMatchProtectedBranchRule(ctx, r.Repository.ID, r.BranchName)
|
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 {
|
if err != nil {
|
||||||
return nil, err
|
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)
|
protectedBranch, err := git_model.GetFirstMatchProtectedBranchRule(ctx, targetRepo.ID, branchName)
|
||||||
|
|
||||||
canEnableEditor := r.CanEnableEditor(ctx, doer)
|
|
||||||
canCommit := canEnableEditor && userCanPush
|
|
||||||
if requireSigned {
|
|
||||||
canCommit = canCommit && sign
|
|
||||||
}
|
|
||||||
wontSignReason := ""
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if asymkey_service.IsErrWontSign(err) {
|
return nil, err
|
||||||
wontSignReason = string(err.(*asymkey_service.ErrWontSign).Reason)
|
}
|
||||||
err = nil
|
canPushWithProtection := true
|
||||||
} else {
|
protectionRequireSigned := false
|
||||||
wontSignReason = "error"
|
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)
|
willSign, signKeyID, _, err := asymkey_service.SignCRUDAction(ctx, targetRepo.RepoPath(), doer, targetRepo.RepoPath(), refName.String())
|
||||||
canCreatePullRequest := ctx.Repo.Repository.UnitEnabled(ctx, unit_model.TypePullRequests) || canCreateBasePullRequest
|
wontSignReason := ""
|
||||||
|
if asymkey_service.IsErrWontSign(err) {
|
||||||
|
wontSignReason = string(err.(*asymkey_service.ErrWontSign).Reason)
|
||||||
|
} else if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
return &CommitFormBehaviors{
|
canCommitToBranch := !submitToForkedRepo /* same repo */ && targetRepo.CanEnableEditor() && canPushWithProtection
|
||||||
CanCommitToBranch: canCommit,
|
if protectionRequireSigned {
|
||||||
EditorEnabled: canEnableEditor,
|
canCommitToBranch = canCommitToBranch && willSign
|
||||||
UserCanPush: userCanPush,
|
}
|
||||||
RequireSigned: requireSigned,
|
|
||||||
WillSign: sign,
|
canCreateBasePullRequest := targetRepo.BaseRepo != nil && targetRepo.BaseRepo.UnitEnabled(ctx, unit_model.TypePullRequests)
|
||||||
SigningKey: keyID,
|
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,
|
WontSignReason: wontSignReason,
|
||||||
|
|
||||||
CanCreatePullRequest: canCreatePullRequest,
|
CanCreatePullRequest: canCreatePullRequest,
|
||||||
CanCreateBasePullRequest: canCreateBasePullRequest,
|
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.
|
// CanUseTimetracker returns whether a user can use the timetracker.
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,9 @@ import (
|
||||||
"regexp"
|
"regexp"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
repo_model "code.gitea.io/gitea/models/repo"
|
||||||
"code.gitea.io/gitea/modules/log"
|
"code.gitea.io/gitea/modules/log"
|
||||||
|
"code.gitea.io/gitea/modules/reqctx"
|
||||||
"code.gitea.io/gitea/modules/setting"
|
"code.gitea.io/gitea/modules/setting"
|
||||||
"code.gitea.io/gitea/services/context"
|
"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["UploadAccepts"] = strings.ReplaceAll(setting.Attachment.AllowedTypes, "|", ",")
|
||||||
ctx.Data["UploadMaxFiles"] = setting.Attachment.MaxFiles
|
ctx.Data["UploadMaxFiles"] = setting.Attachment.MaxFiles
|
||||||
ctx.Data["UploadMaxSize"] = setting.Attachment.MaxSize
|
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:
|
default:
|
||||||
setting.PanicInDevOrTesting("Invalid upload type: %s", uploadType)
|
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"
|
container_module "code.gitea.io/gitea/modules/packages/container"
|
||||||
packages_service "code.gitea.io/gitea/services/packages"
|
packages_service "code.gitea.io/gitea/services/packages"
|
||||||
|
|
||||||
digest "github.com/opencontainers/go-digest"
|
"github.com/opencontainers/go-digest"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Cleanup removes expired container data
|
// Cleanup removes expired container data
|
||||||
|
|
|
||||||
|
|
@ -195,7 +195,7 @@ func (telegramConvertor) WorkflowJob(p *api.WorkflowJobPayload) (TelegramPayload
|
||||||
func createTelegramPayloadHTML(msgHTML string) TelegramPayload {
|
func createTelegramPayloadHTML(msgHTML string) TelegramPayload {
|
||||||
// https://core.telegram.org/bots/api#formatting-options
|
// https://core.telegram.org/bots/api#formatting-options
|
||||||
return TelegramPayload{
|
return TelegramPayload{
|
||||||
Message: strings.TrimSpace(markup.Sanitize(msgHTML)),
|
Message: strings.TrimSpace(string(markup.Sanitize(msgHTML))),
|
||||||
ParseMode: "HTML",
|
ParseMode: "HTML",
|
||||||
DisableWebPreview: true,
|
DisableWebPreview: true,
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -130,8 +130,7 @@
|
||||||
<span class="only-mobile">{{.SignedUser.Name}}</span>
|
<span class="only-mobile">{{.SignedUser.Name}}</span>
|
||||||
<span class="not-mobile">{{svg "octicon-triangle-down"}}</span>
|
<span class="not-mobile">{{svg "octicon-triangle-down"}}</span>
|
||||||
</span>
|
</span>
|
||||||
{{/* do not localize it, here it needs the fixed length (width) to make UI comfortable */}}
|
{{if .IsAdmin}}<span class="navbar-profile-admin">{{svg "octicon-server" 11}}</span>{{end}}
|
||||||
{{if .IsAdmin}}<span class="navbar-profile-admin">admin</span>{{end}}
|
|
||||||
<div class="menu user-menu">
|
<div class="menu user-menu">
|
||||||
<div class="header">
|
<div class="header">
|
||||||
{{ctx.Locale.Tr "signed_in_as"}} <strong>{{.SignedUser.Name}}</strong>
|
{{ctx.Locale.Tr "signed_in_as"}} <strong>{{.SignedUser.Name}}</strong>
|
||||||
|
|
|
||||||
|
|
@ -3,8 +3,9 @@
|
||||||
{{template "repo/header" .}}
|
{{template "repo/header" .}}
|
||||||
<div class="ui container">
|
<div class="ui container">
|
||||||
{{template "base/alert" .}}
|
{{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}}
|
{{.CsrfTokenHtml}}
|
||||||
|
{{template "repo/editor/common_top" .}}
|
||||||
<input type="hidden" name="revert" value="{{if eq .CherryPickType "revert"}}true{{else}}false{{end}}">
|
<input type="hidden" name="revert" value="{{if eq .CherryPickType "revert"}}true{{else}}false{{end}}">
|
||||||
<div class="repo-editor-header">
|
<div class="repo-editor-header">
|
||||||
<div class="breadcrumb">
|
<div class="breadcrumb">
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,11 @@
|
||||||
<div class="commit-form-wrapper">
|
<div class="commit-form-wrapper">
|
||||||
{{ctx.AvatarUtils.Avatar .SignedUser 40 "commit-avatar"}}
|
{{ctx.AvatarUtils.Avatar .SignedUser 40 "commit-avatar"}}
|
||||||
<div class="commit-form">
|
<div class="commit-form">
|
||||||
<h3>{{- if .CommitFormBehaviors.WillSign}}
|
<h3>{{- if .CommitFormOptions.WillSign}}
|
||||||
<span title="{{ctx.Locale.Tr "repo.signing.will_sign" .CommitFormBehaviors.SigningKey}}">{{svg "octicon-lock" 24}}</span>
|
<span title="{{ctx.Locale.Tr "repo.signing.will_sign" .CommitFormOptions.SigningKey}}">{{svg "octicon-lock" 24}}</span>
|
||||||
{{ctx.Locale.Tr "repo.editor.commit_signed_changes"}}
|
{{ctx.Locale.Tr "repo.editor.commit_signed_changes"}}
|
||||||
{{- else}}
|
{{- 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"}}
|
{{ctx.Locale.Tr "repo.editor.commit_changes"}}
|
||||||
{{- end}}</h3>
|
{{- end}}</h3>
|
||||||
<div class="field">
|
<div class="field">
|
||||||
|
|
@ -22,17 +22,17 @@
|
||||||
</div>
|
</div>
|
||||||
<div class="quick-pull-choice js-quick-pull-choice">
|
<div class="quick-pull-choice js-quick-pull-choice">
|
||||||
<div class="field">
|
<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}}>
|
<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>
|
<label>
|
||||||
{{svg "octicon-git-commit"}}
|
{{svg "octicon-git-commit"}}
|
||||||
{{ctx.Locale.Tr "repo.editor.commit_directly_to_this_branch" .BranchName}}
|
{{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">
|
<div class="ui visible small warning message">
|
||||||
{{ctx.Locale.Tr "repo.editor.no_commit_to_branch"}}
|
{{ctx.Locale.Tr "repo.editor.no_commit_to_branch"}}
|
||||||
<ul>
|
<ul>
|
||||||
{{if not .CommitFormBehaviors.UserCanPush}}<li>{{ctx.Locale.Tr "repo.editor.user_no_push_to_branch"}}</li>{{end}}
|
{{if not .CommitFormOptions.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 and .CommitFormOptions.RequireSigned (not .CommitFormOptions.WillSign)}}<li>{{ctx.Locale.Tr "repo.editor.require_signed_commit"}}</li>{{end}}
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
{{end}}
|
{{end}}
|
||||||
|
|
@ -42,14 +42,14 @@
|
||||||
{{if and (not .Repository.IsEmpty) (not .IsEditingFileOnly)}}
|
{{if and (not .Repository.IsEmpty) (not .IsEditingFileOnly)}}
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<div class="ui radio checkbox">
|
<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}}>
|
<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}}
|
{{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}}>
|
<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}}
|
{{end}}
|
||||||
<label>
|
<label>
|
||||||
{{svg "octicon-git-pull-request"}}
|
{{svg "octicon-git-pull-request"}}
|
||||||
{{if .CommitFormBehaviors.CanCreatePullRequest}}
|
{{if .CommitFormOptions.CanCreatePullRequest}}
|
||||||
{{ctx.Locale.Tr "repo.editor.create_new_branch"}}
|
{{ctx.Locale.Tr "repo.editor.create_new_branch"}}
|
||||||
{{else}}
|
{{else}}
|
||||||
{{ctx.Locale.Tr "repo.editor.create_new_branch_np"}}
|
{{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" .}}
|
{{template "repo/header" .}}
|
||||||
<div class="ui container">
|
<div class="ui container">
|
||||||
{{template "base/alert" .}}
|
{{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}}
|
{{.CsrfTokenHtml}}
|
||||||
|
{{template "repo/editor/common_top" .}}
|
||||||
{{template "repo/editor/commit_form" .}}
|
{{template "repo/editor/commit_form" .}}
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -3,11 +3,12 @@
|
||||||
{{template "repo/header" .}}
|
{{template "repo/header" .}}
|
||||||
<div class="ui container">
|
<div class="ui container">
|
||||||
{{template "base/alert" .}}
|
{{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-header="{{ctx.Locale.Tr "repo.editor.commit_empty_file_header"}}"
|
||||||
data-text-empty-confirm-content="{{ctx.Locale.Tr "repo.editor.commit_empty_file_text"}}"
|
data-text-empty-confirm-content="{{ctx.Locale.Tr "repo.editor.commit_empty_file_text"}}"
|
||||||
>
|
>
|
||||||
{{.CsrfTokenHtml}}
|
{{.CsrfTokenHtml}}
|
||||||
|
{{template "repo/editor/common_top" .}}
|
||||||
<div class="repo-editor-header">
|
<div class="repo-editor-header">
|
||||||
{{template "repo/editor/common_breadcrumb" .}}
|
{{template "repo/editor/common_breadcrumb" .}}
|
||||||
</div>
|
</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" .}}
|
{{template "repo/header" .}}
|
||||||
<div class="ui container">
|
<div class="ui container">
|
||||||
{{template "base/alert" .}}
|
{{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-header="{{ctx.Locale.Tr "repo.editor.commit_empty_file_header"}}"
|
||||||
data-text-empty-confirm-content="{{ctx.Locale.Tr "repo.editor.commit_empty_file_text"}}"
|
data-text-empty-confirm-content="{{ctx.Locale.Tr "repo.editor.commit_empty_file_text"}}"
|
||||||
>
|
>
|
||||||
{{.CsrfTokenHtml}}
|
{{.CsrfTokenHtml}}
|
||||||
|
{{template "repo/editor/common_top" .}}
|
||||||
<div class="repo-editor-header">
|
<div class="repo-editor-header">
|
||||||
<div class="breadcrumb">
|
<div class="breadcrumb">
|
||||||
{{ctx.Locale.Tr "repo.editor.patching"}}
|
{{ctx.Locale.Tr "repo.editor.patching"}}
|
||||||
|
|
|
||||||
|
|
@ -3,8 +3,9 @@
|
||||||
{{template "repo/header" .}}
|
{{template "repo/header" .}}
|
||||||
<div class="ui container">
|
<div class="ui container">
|
||||||
{{template "base/alert" .}}
|
{{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}}
|
{{.CsrfTokenHtml}}
|
||||||
|
{{template "repo/editor/common_top" .}}
|
||||||
<div class="repo-editor-header">
|
<div class="repo-editor-header">
|
||||||
{{template "repo/editor/common_breadcrumb" .}}
|
{{template "repo/editor/common_breadcrumb" .}}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -1,22 +1,22 @@
|
||||||
{{if .EscapeStatus}}
|
{{if .EscapeStatus}}
|
||||||
{{if .EscapeStatus.HasInvisible}}
|
{{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>
|
<button class="btn close icon hide-panel" data-panel-closest=".message">{{svg "octicon-x" 16 "close inside"}}</button>
|
||||||
<div class="header">
|
<div class="header">
|
||||||
{{ctx.Locale.Tr "repo.invisible_runes_header"}}
|
{{ctx.Locale.Tr "repo.invisible_runes_header"}}
|
||||||
</div>
|
</div>
|
||||||
<p>{{ctx.Locale.Tr "repo.invisible_runes_description"}}</p>
|
<div>{{ctx.Locale.Tr "repo.invisible_runes_description"}}</div>
|
||||||
{{if .EscapeStatus.HasAmbiguous}}
|
{{if .EscapeStatus.HasAmbiguous}}
|
||||||
<p>{{ctx.Locale.Tr "repo.ambiguous_runes_description"}}</p>
|
<div>{{ctx.Locale.Tr "repo.ambiguous_runes_description"}}</div>
|
||||||
{{end}}
|
{{end}}
|
||||||
</div>
|
</div>
|
||||||
{{else if .EscapeStatus.HasAmbiguous}}
|
{{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>
|
<button class="btn close icon hide-panel" data-panel-closest=".message">{{svg "octicon-x" 16 "close inside"}}</button>
|
||||||
<div class="header">
|
<div class="header">
|
||||||
{{ctx.Locale.Tr "repo.ambiguous_runes_header"}}
|
{{ctx.Locale.Tr "repo.ambiguous_runes_header"}}
|
||||||
</div>
|
</div>
|
||||||
<p>{{ctx.Locale.Tr "repo.ambiguous_runes_description"}}</p>
|
<div>{{ctx.Locale.Tr "repo.ambiguous_runes_description"}}</div>
|
||||||
</div>
|
</div>
|
||||||
{{end}}
|
{{end}}
|
||||||
{{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>
|
<a href="{{.Repository.Link}}/find/{{.RefTypeNameSubURL}}" class="ui compact basic button">{{ctx.Locale.Tr "repo.find_file.go_to_file"}}</a>
|
||||||
{{end}}
|
{{end}}
|
||||||
|
|
||||||
{{if and .CanWriteCode .RefFullName.IsBranch (not .Repository.IsMirror) (not .Repository.IsArchived) (not .IsViewFile)}}
|
{{if and .RefFullName.IsBranch (not .IsViewFile)}}
|
||||||
<button class="ui dropdown basic compact jump button"{{if not .Repository.CanEnableEditor}} disabled{{end}}>
|
<button class="ui dropdown basic compact jump button repo-add-file" {{if not .Repository.CanEnableEditor}}disabled{{end}}>
|
||||||
{{ctx.Locale.Tr "repo.editor.add_file"}}
|
{{ctx.Locale.Tr "repo.editor.add_file"}}
|
||||||
{{svg "octicon-triangle-down" 14 "dropdown icon"}}
|
{{svg "octicon-triangle-down" 14 "dropdown icon"}}
|
||||||
<div class="menu">
|
<div class="menu">
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,7 @@
|
||||||
{{ctx.Locale.Tr "repo.wiki.page_name_desc"}}
|
{{ctx.Locale.Tr "repo.wiki.page_name_desc"}}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{{$content := .content}}
|
{{$content := .WikiEditContent}}
|
||||||
{{if not .PageIsWikiEdit}}
|
{{if not .PageIsWikiEdit}}
|
||||||
{{$content = ctx.Locale.Tr "repo.wiki.welcome"}}
|
{{$content = ctx.Locale.Tr "repo.wiki.welcome"}}
|
||||||
{{end}}
|
{{end}}
|
||||||
|
|
|
||||||
|
|
@ -62,36 +62,34 @@
|
||||||
{{end}}
|
{{end}}
|
||||||
|
|
||||||
<div class="wiki-content-parts">
|
<div class="wiki-content-parts">
|
||||||
{{if .sidebarTocContent}}
|
{{if .WikiSidebarTocHTML}}
|
||||||
<div class="render-content markup wiki-content-sidebar wiki-content-toc">
|
<div class="render-content markup wiki-content-sidebar wiki-content-toc">
|
||||||
{{.sidebarTocContent | SafeHTML}}
|
{{.WikiSidebarTocHTML}}
|
||||||
</div>
|
</div>
|
||||||
{{end}}
|
{{end}}
|
||||||
|
|
||||||
<div class="render-content markup wiki-content-main {{if or .sidebarTocContent .sidebarPresent}}with-sidebar{{end}}">
|
<div class="render-content markup wiki-content-main {{if or .WikiSidebarTocHTML .WikiSidebarHTML}}with-sidebar{{end}}">
|
||||||
{{template "repo/unicode_escape_prompt" dict "EscapeStatus" .EscapeStatus "root" $}}
|
{{template "repo/unicode_escape_prompt" dict "EscapeStatus" .EscapeStatus}}
|
||||||
{{.content | SafeHTML}}
|
{{.WikiContentHTML}}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{{if .sidebarPresent}}
|
{{if .WikiSidebarHTML}}
|
||||||
<div class="render-content markup wiki-content-sidebar">
|
<div class="render-content markup wiki-content-sidebar">
|
||||||
{{if and .CanWriteWiki (not .Repository.IsMirror)}}
|
{{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>
|
<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}}
|
{{end}}
|
||||||
{{template "repo/unicode_escape_prompt" dict "EscapeStatus" .sidebarEscapeStatus "root" $}}
|
{{.WikiSidebarHTML}}
|
||||||
{{.sidebarContent | SafeHTML}}
|
|
||||||
</div>
|
</div>
|
||||||
{{end}}
|
{{end}}
|
||||||
|
|
||||||
<div class="tw-clear-both"></div>
|
<div class="tw-clear-both"></div>
|
||||||
|
|
||||||
{{if .footerPresent}}
|
{{if .WikiFooterHTML}}
|
||||||
<div class="render-content markup wiki-content-footer">
|
<div class="render-content markup wiki-content-footer">
|
||||||
{{if and .CanWriteWiki (not .Repository.IsMirror)}}
|
{{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>
|
<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}}
|
{{end}}
|
||||||
{{template "repo/unicode_escape_prompt" dict "footerEscapeStatus" .sidebarEscapeStatus "root" $}}
|
{{.WikiFooterHTML}}
|
||||||
{{.footerContent | SafeHTML}}
|
|
||||||
</div>
|
</div>
|
||||||
{{end}}
|
{{end}}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
|
"maps"
|
||||||
"mime/multipart"
|
"mime/multipart"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
|
|
@ -19,292 +20,278 @@ import (
|
||||||
"code.gitea.io/gitea/models/unittest"
|
"code.gitea.io/gitea/models/unittest"
|
||||||
user_model "code.gitea.io/gitea/models/user"
|
user_model "code.gitea.io/gitea/models/user"
|
||||||
"code.gitea.io/gitea/modules/git"
|
"code.gitea.io/gitea/modules/git"
|
||||||
"code.gitea.io/gitea/modules/json"
|
|
||||||
"code.gitea.io/gitea/modules/test"
|
"code.gitea.io/gitea/modules/test"
|
||||||
"code.gitea.io/gitea/modules/translation"
|
"code.gitea.io/gitea/modules/translation"
|
||||||
|
"code.gitea.io/gitea/modules/util"
|
||||||
"code.gitea.io/gitea/tests"
|
"code.gitea.io/gitea/tests"
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestCreateFile(t *testing.T) {
|
func TestEditor(t *testing.T) {
|
||||||
onGiteaRun(t, func(t *testing.T, u *url.URL) {
|
onGiteaRun(t, func(t *testing.T, u *url.URL) {
|
||||||
session := loginUser(t, "user2")
|
sessionUser2 := loginUser(t, "user2")
|
||||||
testCreateFile(t, session, "user2", "repo1", "master", "test.txt", "Content")
|
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) {
|
func testCreateFile(t *testing.T, session *TestSession, user, repo, branch, filePath, content string) {
|
||||||
// Request editor page
|
testEditorActionEdit(t, session, user, repo, "_new", branch, "", map[string]string{
|
||||||
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,
|
|
||||||
"tree_path": filePath,
|
"tree_path": filePath,
|
||||||
"content": content,
|
"content": content,
|
||||||
"commit_choice": "direct",
|
"commit_choice": "direct",
|
||||||
})
|
})
|
||||||
resp = session.MakeRequest(t, req, http.StatusOK)
|
|
||||||
assert.NotEmpty(t, test.RedirectURL(resp))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestCreateFileOnProtectedBranch(t *testing.T) {
|
func testEditorProtectedBranch(t *testing.T) {
|
||||||
onGiteaRun(t, func(t *testing.T, u *url.URL) {
|
session := loginUser(t, "user2")
|
||||||
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)
|
// Try to commit a file to the "master" branch and it should fail
|
||||||
// Change master branch to protected
|
resp := testEditorActionPostRequest(t, session, "/user2/repo1/_new/master/", map[string]string{"tree_path": "test-protected-branch.txt", "commit_choice": "direct"})
|
||||||
req := NewRequestWithValues(t, "POST", "/user2/repo1/settings/branches/edit", map[string]string{
|
assert.Equal(t, http.StatusBadRequest, resp.Code)
|
||||||
"_csrf": csrf,
|
assert.Equal(t, `Cannot commit to protected branch "master".`, test.ParseJSONError(resp.Body.Bytes()).ErrorMessage)
|
||||||
"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)
|
|
||||||
|
|
||||||
// Request editor page
|
func testEditorActionPostRequest(t *testing.T, session *TestSession, requestPath string, params map[string]string) *httptest.ResponseRecorder {
|
||||||
req = NewRequest(t, "GET", "/user2/repo1/_new/master/")
|
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)
|
resp := session.MakeRequest(t, req, http.StatusOK)
|
||||||
|
|
||||||
doc := NewHTMLParser(t, resp.Body)
|
respMap := map[string]string{}
|
||||||
lastCommit := doc.GetInputValueByName("last_commit")
|
DecodeJSON(t, resp, &respMap)
|
||||||
assert.NotEmpty(t, lastCommit)
|
return respMap["uuid"]
|
||||||
|
}
|
||||||
|
|
||||||
// Save new file to master branch
|
t.Run("EmailInactive", func(t *testing.T) {
|
||||||
req = NewRequestWithValues(t, "POST", "/user2/repo1/_new/master/", map[string]string{
|
defer tests.PrintCurrentTest(t)()
|
||||||
"_csrf": doc.GetCSRF(),
|
email := unittest.AssertExistsAndLoadBean(t, &user_model.EmailAddress{ID: 35, UID: user.ID})
|
||||||
"last_commit": lastCommit,
|
require.False(t, email.IsActivated)
|
||||||
"tree_path": "test.txt",
|
makeReq(t, "/user2/repo1/_edit/master/README.md", map[string]string{
|
||||||
"content": "Content",
|
"tree_path": "README.md",
|
||||||
"commit_choice": "direct",
|
"content": "test content",
|
||||||
})
|
"commit_email": email.Email,
|
||||||
|
}, "", "")
|
||||||
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)
|
|
||||||
})
|
})
|
||||||
}
|
|
||||||
|
|
||||||
func testEditFile(t *testing.T, session *TestSession, user, repo, branch, filePath, newContent string) *httptest.ResponseRecorder {
|
t.Run("EmailInvalid", func(t *testing.T) {
|
||||||
// Get to the 'edit this file' page
|
defer tests.PrintCurrentTest(t)()
|
||||||
req := NewRequest(t, "GET", path.Join(user, repo, "_edit", branch, filePath))
|
email := unittest.AssertExistsAndLoadBean(t, &user_model.EmailAddress{ID: 1, IsActivated: true})
|
||||||
resp := session.MakeRequest(t, req, http.StatusOK)
|
require.NotEqual(t, email.UID, user.ID)
|
||||||
|
makeReq(t, "/user2/repo1/_edit/master/README.md", map[string]string{
|
||||||
htmlDoc := NewHTMLParser(t, resp.Body)
|
"tree_path": "README.md",
|
||||||
lastCommit := htmlDoc.GetInputValueByName("last_commit")
|
"content": "test content",
|
||||||
assert.NotEmpty(t, lastCommit)
|
"commit_email": email.Email,
|
||||||
|
}, "", "")
|
||||||
// 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")
|
|
||||||
})
|
})
|
||||||
}
|
|
||||||
|
|
||||||
func TestEditFileToNewBranch(t *testing.T) {
|
testWebGit := func(t *testing.T, linkForKeepPrivate string, paramsForKeepPrivate map[string]string, linkForChosenEmail string, paramsForChosenEmail map[string]string) (resp1, resp2 *httptest.ResponseRecorder) {
|
||||||
onGiteaRun(t, func(t *testing.T, u *url.URL) {
|
t.Run("DefaultEmailKeepPrivate", func(t *testing.T) {
|
||||||
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) {
|
|
||||||
defer tests.PrintCurrentTest(t)()
|
defer tests.PrintCurrentTest(t)()
|
||||||
email := unittest.AssertExistsAndLoadBean(t, &user_model.EmailAddress{ID: 35, UID: user.ID})
|
paramsForKeepPrivate["commit_email"] = ""
|
||||||
require.False(t, email.IsActivated)
|
resp1 = makeReq(t, linkForKeepPrivate, paramsForKeepPrivate, "User Two", "user2@noreply.example.org")
|
||||||
makeReq(t, "/user2/repo1/_edit/master/README.md", map[string]string{
|
|
||||||
"tree_path": "README.md",
|
|
||||||
"content": "test content",
|
|
||||||
"commit_email": email.Email,
|
|
||||||
}, "", "")
|
|
||||||
})
|
})
|
||||||
|
t.Run("ChooseEmail", func(t *testing.T) {
|
||||||
t.Run("EmailInvalid", func(t *testing.T) {
|
|
||||||
defer tests.PrintCurrentTest(t)()
|
defer tests.PrintCurrentTest(t)()
|
||||||
email := unittest.AssertExistsAndLoadBean(t, &user_model.EmailAddress{ID: 1, IsActivated: true})
|
paramsForChosenEmail["commit_email"] = "user2@example.com"
|
||||||
require.NotEqual(t, email.UID, user.ID)
|
resp2 = makeReq(t, linkForChosenEmail, paramsForChosenEmail, "User Two", "user2@example.com")
|
||||||
makeReq(t, "/user2/repo1/_edit/master/README.md", map[string]string{
|
|
||||||
"tree_path": "README.md",
|
|
||||||
"content": "test content",
|
|
||||||
"commit_email": email.Email,
|
|
||||||
}, "", "")
|
|
||||||
})
|
})
|
||||||
|
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("Edit", func(t *testing.T) {
|
||||||
t.Run("DefaultEmailKeepPrivate", func(t *testing.T) {
|
testWebGit(t,
|
||||||
defer tests.PrintCurrentTest(t)()
|
"/user2/repo1/_edit/master/README.md", map[string]string{"tree_path": "README.md", "content": "for keep private"},
|
||||||
paramsForKeepPrivate["commit_email"] = ""
|
"/user2/repo1/_edit/master/README.md", map[string]string{"tree_path": "README.md", "content": "for chosen 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) {
|
t.Run("UploadDelete", func(t *testing.T) {
|
||||||
testWebGit(t,
|
file1UUID := uploadFile(t, "file1", "File 1")
|
||||||
"/user2/repo1/_edit/master/README.md", map[string]string{"tree_path": "README.md", "content": "for keep private"},
|
file2UUID := uploadFile(t, "file2", "File 2")
|
||||||
"/user2/repo1/_edit/master/README.md", map[string]string{"tree_path": "README.md", "content": "for chosen email"},
|
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) {
|
t.Run("ApplyPatchCherryPick", func(t *testing.T) {
|
||||||
file1UUID := uploadFile(t, "file1", "File 1")
|
testWebGit(t,
|
||||||
file2UUID := uploadFile(t, "file2", "File 2")
|
"/user2/repo1/_diffpatch/master", map[string]string{
|
||||||
testWebGit(t,
|
"tree_path": "__dummy__",
|
||||||
"/user2/repo1/_upload/master", map[string]string{"files": file1UUID},
|
"content": `diff --git a/patch-file-1.txt b/patch-file-1.txt
|
||||||
"/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
|
|
||||||
new file mode 100644
|
new file mode 100644
|
||||||
index 0000000000..aaaaaaaaaa
|
index 0000000000..aaaaaaaaaa
|
||||||
--- /dev/null
|
--- /dev/null
|
||||||
|
|
@ -312,10 +299,10 @@ index 0000000000..aaaaaaaaaa
|
||||||
@@ -0,0 +1 @@
|
@@ -0,0 +1 @@
|
||||||
+File 1
|
+File 1
|
||||||
`,
|
`,
|
||||||
},
|
},
|
||||||
"/user2/repo1/_diffpatch/master", map[string]string{
|
"/user2/repo1/_diffpatch/master", map[string]string{
|
||||||
"tree_path": "__dummy__",
|
"tree_path": "__dummy__",
|
||||||
"content": `diff --git a/patch-file-2.txt b/patch-file-2.txt
|
"content": `diff --git a/patch-file-2.txt b/patch-file-2.txt
|
||||||
new file mode 100644
|
new file mode 100644
|
||||||
index 0000000000..bbbbbbbbbb
|
index 0000000000..bbbbbbbbbb
|
||||||
--- /dev/null
|
--- /dev/null
|
||||||
|
|
@ -323,20 +310,146 @@ index 0000000000..bbbbbbbbbb
|
||||||
@@ -0,0 +1 @@
|
@@ -0,0 +1 @@
|
||||||
+File 2
|
+File 2
|
||||||
`,
|
`,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
commit1, err := gitRepo.GetCommitByPath("patch-file-1.txt")
|
commit1, err := gitRepo.GetCommitByPath("patch-file-1.txt")
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
commit2, err := gitRepo.GetCommitByPath("patch-file-2.txt")
|
commit2, err := gitRepo.GetCommitByPath("patch-file-2.txt")
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
resp1, _ := testWebGit(t,
|
resp1, _ := testWebGit(t,
|
||||||
"/user2/repo1/_cherrypick/"+commit1.ID.String()+"/master", map[string]string{"revert": "true"},
|
"/user2/repo1/_cherrypick/"+commit1.ID.String()+"/master", map[string]string{"revert": "true"},
|
||||||
"/user2/repo1/_cherrypick/"+commit2.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
|
// 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))
|
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")
|
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) {
|
func AssertHTMLElement[T int | bool](t testing.TB, doc *HTMLDoc, selector string, checkExists T) {
|
||||||
sel := doc.doc.Find(selector)
|
sel := doc.doc.Find(selector)
|
||||||
switch v := any(checkExists).(type) {
|
switch v := any(checkExists).(type) {
|
||||||
|
|
|
||||||
|
|
@ -129,7 +129,7 @@ func TestPullCreate_EmptyChangesWithDifferentCommits(t *testing.T) {
|
||||||
session := loginUser(t, "user1")
|
session := loginUser(t, "user1")
|
||||||
testRepoFork(t, session, "user2", "repo1", "user1", "repo1", "")
|
testRepoFork(t, session, "user2", "repo1", "user1", "repo1", "")
|
||||||
testEditFileToNewBranch(t, session, "user1", "repo1", "master", "status1", "README.md", "status1")
|
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")
|
url := path.Join("user1", "repo1", "compare", "master...status1")
|
||||||
req := NewRequestWithValues(t, "POST", url,
|
req := NewRequestWithValues(t, "POST", url,
|
||||||
|
|
|
||||||
|
|
@ -134,7 +134,9 @@
|
||||||
margin-bottom: 16px;
|
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 {
|
.markup p:last-child {
|
||||||
margin-bottom: 16px;
|
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 {
|
#navbar .ui.dropdown .navbar-profile-admin {
|
||||||
display: block;
|
display: block;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
font-size: 9px;
|
color: var(--color-text);
|
||||||
font-weight: var(--font-weight-bold);
|
background: var(--color-nav-bg);
|
||||||
color: var(--color-nav-bg);
|
border: 1px solid var(--color-nav-bg);
|
||||||
background: var(--color-primary);
|
padding: 2px;
|
||||||
padding: 2px 3px;
|
|
||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
top: -1px;
|
top: -0.5px;
|
||||||
left: 18px;
|
left: 28px;
|
||||||
|
line-height: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
#navbar a.item:hover .notification_count,
|
#navbar a.item:hover .notification_count,
|
||||||
|
|
|
||||||
|
|
@ -1834,6 +1834,7 @@ tbody.commit-list {
|
||||||
border-radius: 0;
|
border-radius: 0;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
gap: 0.5em;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* fomantic's last-child selector does not work with hidden last child */
|
/* fomantic's last-child selector does not work with hidden last child */
|
||||||
|
|
|
||||||
|
|
@ -39,10 +39,6 @@
|
||||||
min-width: 150px;
|
min-width: 150px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.repository.wiki .wiki-content-sidebar .ui.message.unicode-escape-prompt p {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.repository.wiki .wiki-content-footer {
|
.repository.wiki .wiki-content-footer {
|
||||||
margin-top: 1em;
|
margin-top: 1em;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
// TODO: Switch to upstream after https://github.com/razorness/vue3-calendar-heatmap/pull/34 is merged
|
// TODO: Switch to upstream after https://github.com/razorness/vue3-calendar-heatmap/pull/34 is merged
|
||||||
import {CalendarHeatmap} from '@silverwind/vue3-calendar-heatmap';
|
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';
|
import type {Value as HeatmapValue, Locale as HeatmapLocale} from '@silverwind/vue3-calendar-heatmap';
|
||||||
|
|
||||||
defineProps<{
|
defineProps<{
|
||||||
|
|
@ -24,7 +24,7 @@ const colorRange = [
|
||||||
'var(--color-primary-dark-4)',
|
'var(--color-primary-dark-4)',
|
||||||
];
|
];
|
||||||
|
|
||||||
const endDate = ref(new Date());
|
const endDate = shallowRef(new Date());
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
// work around issue with first legend color being rendered twice and legend cut off
|
// work around issue with first legend color being rendered twice and legend cut off
|
||||||
|
|
|
||||||
|
|
@ -2,16 +2,16 @@
|
||||||
import {SvgIcon} from '../svg.ts';
|
import {SvgIcon} from '../svg.ts';
|
||||||
import {GET} from '../modules/fetch.ts';
|
import {GET} from '../modules/fetch.ts';
|
||||||
import {getIssueColor, getIssueIcon} from '../features/issue.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';
|
import type {IssuePathInfo} from '../types.ts';
|
||||||
|
|
||||||
const {appSubUrl, i18n} = window.config;
|
const {appSubUrl, i18n} = window.config;
|
||||||
|
|
||||||
const loading = ref(false);
|
const loading = shallowRef(false);
|
||||||
const issue = ref(null);
|
const issue = shallowRef(null);
|
||||||
const renderedLabels = ref('');
|
const renderedLabels = shallowRef('');
|
||||||
const i18nErrorOccurred = i18n.error_occurred;
|
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 createdAt = computed(() => new Date(issue.value.created_at).toLocaleDateString(undefined, {year: 'numeric', month: 'short', day: 'numeric'}));
|
||||||
const body = computed(() => {
|
const body = computed(() => {
|
||||||
|
|
@ -22,7 +22,7 @@ const body = computed(() => {
|
||||||
return body;
|
return body;
|
||||||
});
|
});
|
||||||
|
|
||||||
const root = ref<HTMLElement | null>(null);
|
const root = useTemplateRef('root');
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
root.value.addEventListener('ce-load-context-popup', (e: CustomEventInit<IssuePathInfo>) => {
|
root.value.addEventListener('ce-load-context-popup', (e: CustomEventInit<IssuePathInfo>) => {
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import {SvgIcon, type SvgName} from '../svg.ts';
|
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';
|
import {type DiffStatus, type DiffTreeEntry, diffTreeStore} from '../modules/diff-file.ts';
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
|
|
@ -8,7 +8,7 @@ const props = defineProps<{
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const store = diffTreeStore();
|
const store = diffTreeStore();
|
||||||
const collapsed = ref(props.item.IsViewed);
|
const collapsed = shallowRef(props.item.IsViewed);
|
||||||
|
|
||||||
function getIconForDiffStatus(pType: DiffStatus) {
|
function getIconForDiffStatus(pType: DiffStatus) {
|
||||||
const diffTypes: Record<DiffStatus, { name: SvgName, classes: Array<string> }> = {
|
const diffTypes: Record<DiffStatus, { name: SvgName, classes: Array<string> }> = {
|
||||||
|
|
|
||||||
|
|
@ -1,19 +1,19 @@
|
||||||
<script lang="ts" setup>
|
<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 {SvgIcon} from '../svg.ts';
|
||||||
import {toggleElem} from '../utils/dom.ts';
|
import {toggleElem} from '../utils/dom.ts';
|
||||||
|
|
||||||
const {csrfToken, pageData} = window.config;
|
const {csrfToken, pageData} = window.config;
|
||||||
|
|
||||||
const mergeForm = ref(pageData.pullRequestMergeForm);
|
const mergeForm = pageData.pullRequestMergeForm;
|
||||||
|
|
||||||
const mergeTitleFieldValue = ref('');
|
const mergeTitleFieldValue = shallowRef('');
|
||||||
const mergeMessageFieldValue = ref('');
|
const mergeMessageFieldValue = shallowRef('');
|
||||||
const deleteBranchAfterMerge = ref(false);
|
const deleteBranchAfterMerge = shallowRef(false);
|
||||||
const autoMergeWhenSucceed = ref(false);
|
const autoMergeWhenSucceed = shallowRef(false);
|
||||||
|
|
||||||
const mergeStyle = ref('');
|
const mergeStyle = shallowRef('');
|
||||||
const mergeStyleDetail = ref({
|
const mergeStyleDetail = shallowRef({
|
||||||
hideMergeMessageTexts: false,
|
hideMergeMessageTexts: false,
|
||||||
textDoMerge: '',
|
textDoMerge: '',
|
||||||
mergeTitleFieldText: '',
|
mergeTitleFieldText: '',
|
||||||
|
|
@ -21,33 +21,33 @@ const mergeStyleDetail = ref({
|
||||||
hideAutoMerge: false,
|
hideAutoMerge: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
const mergeStyleAllowedCount = ref(0);
|
const mergeStyleAllowedCount = shallowRef(0);
|
||||||
|
|
||||||
const showMergeStyleMenu = ref(false);
|
const showMergeStyleMenu = shallowRef(false);
|
||||||
const showActionForm = ref(false);
|
const showActionForm = shallowRef(false);
|
||||||
|
|
||||||
const mergeButtonStyleClass = computed(() => {
|
const mergeButtonStyleClass = computed(() => {
|
||||||
if (mergeForm.value.allOverridableChecksOk) return 'primary';
|
if (mergeForm.allOverridableChecksOk) return 'primary';
|
||||||
return autoMergeWhenSucceed.value ? 'primary' : 'red';
|
return autoMergeWhenSucceed.value ? 'primary' : 'red';
|
||||||
});
|
});
|
||||||
|
|
||||||
const forceMerge = computed(() => {
|
const forceMerge = computed(() => {
|
||||||
return mergeForm.value.canMergeNow && !mergeForm.value.allOverridableChecksOk;
|
return mergeForm.canMergeNow && !mergeForm.allOverridableChecksOk;
|
||||||
});
|
});
|
||||||
|
|
||||||
watch(mergeStyle, (val) => {
|
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]')) {
|
for (const elem of document.querySelectorAll('[data-pull-merge-style]')) {
|
||||||
toggleElem(elem, elem.getAttribute('data-pull-merge-style') === val);
|
toggleElem(elem, elem.getAttribute('data-pull-merge-style') === val);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
onMounted(() => {
|
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;
|
let mergeStyle = mergeForm.mergeStyles.find((e: any) => e.allowed && e.name === mergeForm.defaultMergeStyle)?.name;
|
||||||
if (!mergeStyle) mergeStyle = mergeForm.value.mergeStyles.find((e: any) => e.allowed)?.name;
|
if (!mergeStyle) mergeStyle = mergeForm.mergeStyles.find((e: any) => e.allowed)?.name;
|
||||||
switchMergeStyle(mergeStyle, !mergeForm.value.canMergeNow);
|
switchMergeStyle(mergeStyle, !mergeForm.canMergeNow);
|
||||||
|
|
||||||
document.addEventListener('mouseup', hideMergeStyleMenu);
|
document.addEventListener('mouseup', hideMergeStyleMenu);
|
||||||
});
|
});
|
||||||
|
|
@ -63,7 +63,7 @@ function hideMergeStyleMenu() {
|
||||||
function toggleActionForm(show: boolean) {
|
function toggleActionForm(show: boolean) {
|
||||||
showActionForm.value = show;
|
showActionForm.value = show;
|
||||||
if (!show) return;
|
if (!show) return;
|
||||||
deleteBranchAfterMerge.value = mergeForm.value.defaultDeleteBranchAfterMerge;
|
deleteBranchAfterMerge.value = mergeForm.defaultDeleteBranchAfterMerge;
|
||||||
mergeTitleFieldValue.value = mergeStyleDetail.value.mergeTitleFieldText;
|
mergeTitleFieldValue.value = mergeStyleDetail.value.mergeTitleFieldText;
|
||||||
mergeMessageFieldValue.value = mergeStyleDetail.value.mergeMessageFieldText;
|
mergeMessageFieldValue.value = mergeStyleDetail.value.mergeMessageFieldText;
|
||||||
}
|
}
|
||||||
|
|
@ -74,7 +74,7 @@ function switchMergeStyle(name: string, autoMerge = false) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function clearMergeMessage() {
|
function clearMergeMessage() {
|
||||||
mergeMessageFieldValue.value = mergeForm.value.defaultMergeMessage;
|
mergeMessageFieldValue.value = mergeForm.defaultMergeMessage;
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,9 @@
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
// @ts-expect-error - module exports no types
|
// @ts-expect-error - module exports no types
|
||||||
import {VueBarGraph} from 'vue-bar-graph';
|
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',
|
barColor: 'green',
|
||||||
textColor: 'black',
|
textColor: 'black',
|
||||||
textAltColor: 'white',
|
textAltColor: 'white',
|
||||||
|
|
@ -41,8 +41,8 @@ const graphWidth = computed(() => {
|
||||||
return activityTopAuthors.length * 40;
|
return activityTopAuthors.length * 40;
|
||||||
});
|
});
|
||||||
|
|
||||||
const styleElement = ref<HTMLElement | null>(null);
|
const styleElement = useTemplateRef('styleElement');
|
||||||
const altStyleElement = ref<HTMLElement | null>(null);
|
const altStyleElement = useTemplateRef('altStyleElement');
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
const refStyle = window.getComputedStyle(styleElement.value);
|
const refStyle = window.getComputedStyle(styleElement.value);
|
||||||
|
|
|
||||||
|
|
@ -23,7 +23,7 @@ import {
|
||||||
import {chartJsColors} from '../utils/color.ts';
|
import {chartJsColors} from '../utils/color.ts';
|
||||||
import {sleep} from '../utils.ts';
|
import {sleep} from '../utils.ts';
|
||||||
import 'chartjs-adapter-dayjs-4/dist/chartjs-adapter-dayjs-4.esm';
|
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;
|
const {pageData} = window.config;
|
||||||
|
|
||||||
|
|
@ -47,10 +47,10 @@ defineProps<{
|
||||||
};
|
};
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const isLoading = ref(false);
|
const isLoading = shallowRef(false);
|
||||||
const errorText = ref('');
|
const errorText = shallowRef('');
|
||||||
const repoLink = ref(pageData.repoLink || []);
|
const repoLink = pageData.repoLink;
|
||||||
const data = ref<DayData[]>([]);
|
const data = shallowRef<DayData[]>([]);
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
fetchGraphData();
|
fetchGraphData();
|
||||||
|
|
@ -61,7 +61,7 @@ async function fetchGraphData() {
|
||||||
try {
|
try {
|
||||||
let response: Response;
|
let response: Response;
|
||||||
do {
|
do {
|
||||||
response = await GET(`${repoLink.value}/activity/code-frequency/data`);
|
response = await GET(`${repoLink}/activity/code-frequency/data`);
|
||||||
if (response.status === 202) {
|
if (response.status === 202) {
|
||||||
await sleep(1000); // wait for 1 second before retrying
|
await sleep(1000); // wait for 1 second before retrying
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -21,7 +21,7 @@ import {
|
||||||
import {chartJsColors} from '../utils/color.ts';
|
import {chartJsColors} from '../utils/color.ts';
|
||||||
import {sleep} from '../utils.ts';
|
import {sleep} from '../utils.ts';
|
||||||
import 'chartjs-adapter-dayjs-4/dist/chartjs-adapter-dayjs-4.esm';
|
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;
|
const {pageData} = window.config;
|
||||||
|
|
||||||
|
|
@ -43,9 +43,9 @@ defineProps<{
|
||||||
};
|
};
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const isLoading = ref(false);
|
const isLoading = shallowRef(false);
|
||||||
const errorText = ref('');
|
const errorText = shallowRef('');
|
||||||
const repoLink = ref(pageData.repoLink || []);
|
const repoLink = pageData.repoLink;
|
||||||
const data = ref<DayData[]>([]);
|
const data = ref<DayData[]>([]);
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
|
|
@ -57,7 +57,7 @@ async function fetchGraphData() {
|
||||||
try {
|
try {
|
||||||
let response: Response;
|
let response: Response;
|
||||||
do {
|
do {
|
||||||
response = await GET(`${repoLink.value}/activity/recent-commits/data`);
|
response = await GET(`${repoLink}/activity/recent-commits/data`);
|
||||||
if (response.status === 202) {
|
if (response.status === 202) {
|
||||||
await sleep(1000); // wait for 1 second before retrying
|
await sleep(1000); // wait for 1 second before retrying
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,9 @@
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import ViewFileTreeItem from './ViewFileTreeItem.vue';
|
import ViewFileTreeItem from './ViewFileTreeItem.vue';
|
||||||
import {onMounted, ref} from 'vue';
|
import {onMounted, useTemplateRef} from 'vue';
|
||||||
import {createViewFileTreeStore} from './ViewFileTreeStore.ts';
|
import {createViewFileTreeStore} from './ViewFileTreeStore.ts';
|
||||||
|
|
||||||
const elRoot = ref<HTMLElement | null>(null);
|
const elRoot = useTemplateRef('elRoot');
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
repoLink: {type: String, required: true},
|
repoLink: {type: String, required: true},
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import {SvgIcon} from '../svg.ts';
|
import {SvgIcon} from '../svg.ts';
|
||||||
import {isPlainClick} from '../utils/dom.ts';
|
import {isPlainClick} from '../utils/dom.ts';
|
||||||
import {ref} from 'vue';
|
import {shallowRef} from 'vue';
|
||||||
import {type createViewFileTreeStore} from './ViewFileTreeStore.ts';
|
import {type createViewFileTreeStore} from './ViewFileTreeStore.ts';
|
||||||
|
|
||||||
type Item = {
|
type Item = {
|
||||||
|
|
@ -20,9 +20,9 @@ const props = defineProps<{
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const store = props.store;
|
const store = props.store;
|
||||||
const isLoading = ref(false);
|
const isLoading = shallowRef(false);
|
||||||
const children = ref(props.item.children);
|
const children = shallowRef(props.item.children);
|
||||||
const collapsed = ref(!props.item.children);
|
const collapsed = shallowRef(!props.item.children);
|
||||||
|
|
||||||
const doLoadChildren = async () => {
|
const doLoadChildren = async () => {
|
||||||
collapsed.value = !collapsed.value;
|
collapsed.value = !collapsed.value;
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@ import {confirmModal} from './comp/ConfirmModal.ts';
|
||||||
import type {RequestOpts} from '../types.ts';
|
import type {RequestOpts} from '../types.ts';
|
||||||
import {ignoreAreYouSure} from '../vendor/jquery.are-you-sure.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"
|
// fetchActionDoRedirect does real redirection to bypass the browser's limitations of "location"
|
||||||
// more details are in the backend's fetch-redirect handler
|
// 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) {
|
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 {
|
try {
|
||||||
hideToastsAll();
|
hideToastsAll();
|
||||||
const resp = await request(url, opt);
|
const resp = await request(url, opt);
|
||||||
if (resp.status === 200) {
|
respStatus = resp.status;
|
||||||
let {redirect} = await resp.json();
|
respText = await resp.text();
|
||||||
|
const respJson = JSON.parse(respText);
|
||||||
|
if (respStatus === 200) {
|
||||||
|
let {redirect} = respJson;
|
||||||
redirect = redirect || actionElem.getAttribute('data-redirect');
|
redirect = redirect || actionElem.getAttribute('data-redirect');
|
||||||
ignoreAreYouSure(actionElem); // ignore the areYouSure check before reloading
|
ignoreAreYouSure(actionElem); // ignore the areYouSure check before reloading
|
||||||
if (redirect) {
|
if (redirect) {
|
||||||
|
|
@ -38,22 +47,19 @@ async function fetchActionDoRequest(actionElem: HTMLElement, url: string, opt: R
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (resp.status >= 400 && resp.status < 500) {
|
if (respStatus >= 400 && respStatus < 500 && respJson?.errorMessage) {
|
||||||
const data = await resp.json();
|
|
||||||
// the code was quite messy, sometimes the backend uses "err", sometimes it uses "error", and even "user_error"
|
// 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.
|
// but at the moment, as a new approach, we only use "errorMessage" here, backend can use JSONError() to respond.
|
||||||
if (data.errorMessage) {
|
showErrorToast(respJson.errorMessage, {useHtmlBody: respJson.renderFormat === 'html'});
|
||||||
showErrorToast(data.errorMessage, {useHtmlBody: data.renderFormat === 'html'});
|
|
||||||
} else {
|
|
||||||
showErrorToast(`server error: ${resp.status}`);
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
showErrorToast(`server error: ${resp.status}`);
|
showErrorForResponse(respStatus, respText);
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (e.name !== 'AbortError') {
|
if (e.name === 'SyntaxError') {
|
||||||
console.error('error when doRequest', e);
|
showErrorForResponse(respStatus, (respText || '').substring(0, 100));
|
||||||
showErrorToast(`${i18n.network_error} ${e}`);
|
} else if (e.name !== 'AbortError') {
|
||||||
|
console.error('fetchActionDoRequest error', e);
|
||||||
|
showErrorForResponse(respStatus, `${e}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
actionElem.classList.remove('is-loading', 'loading-icon-2px');
|
actionElem.classList.remove('is-loading', 'loading-icon-2px');
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue