diff options
169 files changed, 2547 insertions, 2899 deletions
diff --git a/models/git/lfs.go b/models/git/lfs.go index bb6361050a..e4fa2b446a 100644 --- a/models/git/lfs.go +++ b/models/git/lfs.go @@ -112,7 +112,6 @@ type LFSMetaObject struct { ID int64 `xorm:"pk autoincr"` lfs.Pointer `xorm:"extends"` RepositoryID int64 `xorm:"UNIQUE(s) INDEX NOT NULL"` - Existing bool `xorm:"-"` CreatedUnix timeutil.TimeStamp `xorm:"created"` UpdatedUnix timeutil.TimeStamp `xorm:"INDEX updated"` } @@ -146,7 +145,6 @@ func NewLFSMetaObject(ctx context.Context, repoID int64, p lfs.Pointer) (*LFSMet if err != nil { return nil, err } else if exist { - m.Existing = true return m, committer.Commit() } diff --git a/models/repo/repo.go b/models/repo/repo.go index 5aae02c6d8..34d1bf55f6 100644 --- a/models/repo/repo.go +++ b/models/repo/repo.go @@ -653,7 +653,7 @@ func (repo *Repository) AllowsPulls(ctx context.Context) bool { // CanEnableEditor returns true if repository meets the requirements of web editor. func (repo *Repository) CanEnableEditor() bool { - return !repo.IsMirror + return !repo.IsMirror && !repo.IsArchived } // DescriptionHTML does special handles to description and return HTML string. diff --git a/models/user/user.go b/models/user/user.go index 86a3549345..7c871bf575 100644 --- a/models/user/user.go +++ b/models/user/user.go @@ -831,6 +831,20 @@ type CountUserFilter struct { IsActive optional.Option[bool] } +// HasUsers checks whether there are any users in the database, or only one user exists. +func HasUsers(ctx context.Context) (ret struct { + HasAnyUser, HasOnlyOneUser bool +}, err error, +) { + res, err := db.GetEngine(ctx).Table(&User{}).Cols("id").Limit(2).Query() + if err != nil { + return ret, fmt.Errorf("error checking user existence: %w", err) + } + ret.HasAnyUser = len(res) != 0 + ret.HasOnlyOneUser = len(res) == 1 + return ret, nil +} + // CountUsers returns number of users. func CountUsers(ctx context.Context, opts *CountUserFilter) int64 { return countUsers(ctx, opts) diff --git a/modules/htmlutil/html.go b/modules/htmlutil/html.go index 0ab0e71689..194135ba18 100644 --- a/modules/htmlutil/html.go +++ b/modules/htmlutil/html.go @@ -7,6 +7,7 @@ import ( "fmt" "html/template" "slices" + "strings" ) // ParseSizeAndClass get size and class from string with default values @@ -31,6 +32,9 @@ func ParseSizeAndClass(defaultSize int, defaultClass string, others ...any) (int } func HTMLFormat(s template.HTML, rawArgs ...any) template.HTML { + if !strings.Contains(string(s), "%") || len(rawArgs) == 0 { + panic("HTMLFormat requires one or more arguments") + } args := slices.Clone(rawArgs) for i, v := range args { switch v := v.(type) { diff --git a/modules/markup/html_node.go b/modules/markup/html_node.go index f67437465c..4eb78fdd2b 100644 --- a/modules/markup/html_node.go +++ b/modules/markup/html_node.go @@ -63,8 +63,11 @@ func processNodeA(ctx *RenderContext, node *html.Node) { func visitNodeImg(ctx *RenderContext, img *html.Node) (next *html.Node) { next = img.NextSibling + attrSrc, hasLazy := "", false for i, imgAttr := range img.Attr { + hasLazy = hasLazy || imgAttr.Key == "loading" && imgAttr.Val == "lazy" if imgAttr.Key != "src" { + attrSrc = imgAttr.Val continue } @@ -72,8 +75,8 @@ func visitNodeImg(ctx *RenderContext, img *html.Node) (next *html.Node) { isLinkable := imgSrcOrigin != "" && !strings.HasPrefix(imgSrcOrigin, "data:") // By default, the "<img>" tag should also be clickable, - // because frontend use `<img>` to paste the re-scaled image into the markdown, - // so it must match the default markdown image behavior. + // because frontend uses `<img>` to paste the re-scaled image into the Markdown, + // so it must match the default Markdown image behavior. cnt := 0 for p := img.Parent; isLinkable && p != nil && cnt < 2; p = p.Parent { if hasParentAnchor := p.Type == html.ElementNode && p.Data == "a"; hasParentAnchor { @@ -98,6 +101,9 @@ func visitNodeImg(ctx *RenderContext, img *html.Node) (next *html.Node) { imgAttr.Val = camoHandleLink(imgAttr.Val) img.Attr[i] = imgAttr } + if !RenderBehaviorForTesting.DisableAdditionalAttributes && !hasLazy && !strings.HasPrefix(attrSrc, "data:") { + img.Attr = append(img.Attr, html.Attribute{Key: "loading", Val: "lazy"}) + } return next } diff --git a/modules/markup/markdown/markdown_test.go b/modules/markup/markdown/markdown_test.go index 76434ac8b3..4eb01bcc2d 100644 --- a/modules/markup/markdown/markdown_test.go +++ b/modules/markup/markdown/markdown_test.go @@ -47,7 +47,7 @@ func TestRender_StandardLinks(t *testing.T) { func TestRender_Images(t *testing.T) { setting.AppURL = AppURL - test := func(input, expected string) { + render := func(input, expected string) { buffer, err := markdown.RenderString(markup.NewTestRenderContext(FullURL), input) assert.NoError(t, err) assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(string(buffer))) @@ -59,27 +59,32 @@ func TestRender_Images(t *testing.T) { result := util.URLJoin(FullURL, url) // hint: With Markdown v2.5.2, there is a new syntax: [link](URL){:target="_blank"} , but we do not support it now - test( + render( "", `<p><a href="`+result+`" target="_blank" rel="nofollow noopener"><img src="`+result+`" alt="`+title+`"/></a></p>`) - test( + render( "[["+title+"|"+url+"]]", `<p><a href="`+result+`" rel="nofollow"><img src="`+result+`" title="`+title+`" alt="`+title+`"/></a></p>`) - test( + render( "[]("+href+")", `<p><a href="`+href+`" rel="nofollow"><img src="`+result+`" alt="`+title+`"/></a></p>`) - test( + render( "", `<p><a href="`+result+`" target="_blank" rel="nofollow noopener"><img src="`+result+`" alt="`+title+`"/></a></p>`) - test( + render( "[["+title+"|"+url+"]]", `<p><a href="`+result+`" rel="nofollow"><img src="`+result+`" title="`+title+`" alt="`+title+`"/></a></p>`) - test( + render( "[]("+href+")", `<p><a href="`+href+`" rel="nofollow"><img src="`+result+`" alt="`+title+`"/></a></p>`) + + defer test.MockVariableValue(&markup.RenderBehaviorForTesting.DisableAdditionalAttributes, false)() + render( + "<a><img src='a.jpg'></a>", // by the way, empty "a" tag will be removed + `<p dir="auto"><img src="http://localhost:3000/user13/repo11/a.jpg" loading="lazy"/></p>`) } func TestTotal_RenderString(t *testing.T) { diff --git a/modules/markup/markdown/math/block_renderer.go b/modules/markup/markdown/math/block_renderer.go index 427ed842ec..95a336a02c 100644 --- a/modules/markup/markdown/math/block_renderer.go +++ b/modules/markup/markdown/math/block_renderer.go @@ -51,8 +51,8 @@ func (r *BlockRenderer) writeLines(w util.BufWriter, source []byte, n gast.Node) func (r *BlockRenderer) renderBlock(w util.BufWriter, source []byte, node gast.Node, entering bool) (gast.WalkStatus, error) { n := node.(*Block) if entering { - code := giteaUtil.Iif(n.Inline, "", `<pre class="code-block is-loading">`) + `<code class="language-math display">` - _ = r.renderInternal.FormatWithSafeAttrs(w, template.HTML(code)) + codeHTML := giteaUtil.Iif[template.HTML](n.Inline, "", `<pre class="code-block is-loading">`) + `<code class="language-math display">` + _, _ = w.WriteString(string(r.renderInternal.ProtectSafeAttrs(codeHTML))) r.writeLines(w, source, n) } else { _, _ = w.WriteString(`</code>` + giteaUtil.Iif(n.Inline, "", `</pre>`) + "\n") diff --git a/modules/markup/markdown/math/inline_renderer.go b/modules/markup/markdown/math/inline_renderer.go index d000a7b317..eeeb60cc7e 100644 --- a/modules/markup/markdown/math/inline_renderer.go +++ b/modules/markup/markdown/math/inline_renderer.go @@ -28,7 +28,7 @@ func NewInlineRenderer(renderInternal *internal.RenderInternal) renderer.NodeRen func (r *InlineRenderer) renderInline(w util.BufWriter, source []byte, n ast.Node, entering bool) (ast.WalkStatus, error) { if entering { - _ = r.renderInternal.FormatWithSafeAttrs(w, `<code class="language-math">`) + _, _ = w.WriteString(string(r.renderInternal.ProtectSafeAttrs(`<code class="language-math">`))) for c := n.FirstChild(); c != nil; c = c.NextSibling() { segment := c.(*ast.Text).Segment value := util.EscapeHTML(segment.Value(source)) diff --git a/modules/markup/sanitizer_default.go b/modules/markup/sanitizer_default.go index 14161eb533..0fbf0f0b24 100644 --- a/modules/markup/sanitizer_default.go +++ b/modules/markup/sanitizer_default.go @@ -4,6 +4,7 @@ package markup import ( + "html/template" "io" "net/url" "regexp" @@ -52,6 +53,8 @@ func (st *Sanitizer) createDefaultPolicy() *bluemonday.Policy { policy.AllowAttrs("src", "autoplay", "controls").OnElements("video") + policy.AllowAttrs("loading").OnElements("img") + // Allow generally safe attributes (reference: https://github.com/jch/html-pipeline) generalSafeAttrs := []string{ "abbr", "accept", "accept-charset", @@ -90,9 +93,9 @@ func (st *Sanitizer) createDefaultPolicy() *bluemonday.Policy { return policy } -// Sanitize takes a string that contains a HTML fragment or document and applies policy whitelist. -func Sanitize(s string) string { - return GetDefaultSanitizer().defaultPolicy.Sanitize(s) +// Sanitize use default sanitizer policy to sanitize a string +func Sanitize(s string) template.HTML { + return template.HTML(GetDefaultSanitizer().defaultPolicy.Sanitize(s)) } // SanitizeReader sanitizes a Reader diff --git a/modules/markup/sanitizer_default_test.go b/modules/markup/sanitizer_default_test.go index 5282916944..e5ba018e1b 100644 --- a/modules/markup/sanitizer_default_test.go +++ b/modules/markup/sanitizer_default_test.go @@ -69,6 +69,6 @@ func TestSanitizer(t *testing.T) { } for i := 0; i < len(testCases); i += 2 { - assert.Equal(t, testCases[i+1], Sanitize(testCases[i])) + assert.Equal(t, testCases[i+1], string(Sanitize(testCases[i]))) } } diff --git a/modules/structs/repo_file.go b/modules/structs/repo_file.go index b0e0bd979e..a281620a3b 100644 --- a/modules/structs/repo_file.go +++ b/modules/structs/repo_file.go @@ -66,6 +66,8 @@ func (o *UpdateFileOptions) Branch() string { return o.FileOptions.BranchName } +// FIXME: ChangeFileOperation.SHA is NOT required for update or delete if last commit is provided in the options. + // ChangeFileOperation for creating, updating or deleting a file type ChangeFileOperation struct { // indicates what to do with the file diff --git a/modules/templates/helper.go b/modules/templates/helper.go index d55d4f87c5..ff3f7cfda1 100644 --- a/modules/templates/helper.go +++ b/modules/templates/helper.go @@ -6,7 +6,6 @@ package templates import ( "fmt" - "html" "html/template" "net/url" "strconv" @@ -38,9 +37,7 @@ func NewFuncMap() template.FuncMap { "dict": dict, // it's lowercase because this name has been widely used. Our other functions should have uppercase names. "Iif": iif, "Eval": evalTokens, - "SafeHTML": safeHTML, "HTMLFormat": htmlFormat, - "HTMLEscape": htmlEscape, "QueryEscape": queryEscape, "QueryBuild": QueryBuild, "JSEscape": jsEscapeSafe, @@ -165,30 +162,9 @@ func NewFuncMap() template.FuncMap { } } -// safeHTML render raw as HTML -func safeHTML(s any) template.HTML { - switch v := s.(type) { - case string: - return template.HTML(v) - case template.HTML: - return v - } - panic(fmt.Sprintf("unexpected type %T", s)) -} - -// SanitizeHTML sanitizes the input by pre-defined markdown rules +// SanitizeHTML sanitizes the input by default sanitization rules. func SanitizeHTML(s string) template.HTML { - return template.HTML(markup.Sanitize(s)) -} - -func htmlEscape(s any) template.HTML { - switch v := s.(type) { - case string: - return template.HTML(html.EscapeString(v)) - case template.HTML: - return v - } - panic(fmt.Sprintf("unexpected type %T", s)) + return markup.Sanitize(s) } func htmlFormat(s any, args ...any) template.HTML { diff --git a/modules/web/router_path.go b/modules/web/router_path.go index 1531ccd01c..ce041eedab 100644 --- a/modules/web/router_path.go +++ b/modules/web/router_path.go @@ -6,6 +6,7 @@ package web import ( "net/http" "regexp" + "slices" "strings" "code.gitea.io/gitea/modules/container" @@ -36,11 +37,21 @@ func (g *RouterPathGroup) ServeHTTP(resp http.ResponseWriter, req *http.Request) g.r.chiRouter.NotFoundHandler().ServeHTTP(resp, req) } +type RouterPathGroupPattern struct { + re *regexp.Regexp + params []routerPathParam + middlewares []any +} + // MatchPath matches the request method, and uses regexp to match the path. -// The pattern uses "<...>" to define path parameters, for example: "/<name>" (different from chi router) -// It is only designed to resolve some special cases which chi router can't handle. +// The pattern uses "<...>" to define path parameters, for example, "/<name>" (different from chi router) +// It is only designed to resolve some special cases that chi router can't handle. // For most cases, it shouldn't be used because it needs to iterate all rules to find the matched one (inefficient). func (g *RouterPathGroup) MatchPath(methods, pattern string, h ...any) { + g.MatchPattern(methods, g.PatternRegexp(pattern), h...) +} + +func (g *RouterPathGroup) MatchPattern(methods string, pattern *RouterPathGroupPattern, h ...any) { g.matchers = append(g.matchers, newRouterPathMatcher(methods, pattern, h...)) } @@ -96,8 +107,8 @@ func isValidMethod(name string) bool { return false } -func newRouterPathMatcher(methods, pattern string, h ...any) *routerPathMatcher { - middlewares, handlerFunc := wrapMiddlewareAndHandler(nil, h) +func newRouterPathMatcher(methods string, patternRegexp *RouterPathGroupPattern, h ...any) *routerPathMatcher { + middlewares, handlerFunc := wrapMiddlewareAndHandler(patternRegexp.middlewares, h) p := &routerPathMatcher{methods: make(container.Set[string]), middlewares: middlewares, handlerFunc: handlerFunc} for method := range strings.SplitSeq(methods, ",") { method = strings.TrimSpace(method) @@ -106,19 +117,25 @@ func newRouterPathMatcher(methods, pattern string, h ...any) *routerPathMatcher } p.methods.Add(method) } + p.re, p.params = patternRegexp.re, patternRegexp.params + return p +} + +func patternRegexp(pattern string, h ...any) *RouterPathGroupPattern { + p := &RouterPathGroupPattern{middlewares: slices.Clone(h)} re := []byte{'^'} lastEnd := 0 for lastEnd < len(pattern) { start := strings.IndexByte(pattern[lastEnd:], '<') if start == -1 { - re = append(re, pattern[lastEnd:]...) + re = append(re, regexp.QuoteMeta(pattern[lastEnd:])...) break } end := strings.IndexByte(pattern[lastEnd+start:], '>') if end == -1 { panic("invalid pattern: " + pattern) } - re = append(re, pattern[lastEnd:lastEnd+start]...) + re = append(re, regexp.QuoteMeta(pattern[lastEnd:lastEnd+start])...) partName, partExp, _ := strings.Cut(pattern[lastEnd+start+1:lastEnd+start+end], ":") lastEnd += start + end + 1 @@ -140,7 +157,10 @@ func newRouterPathMatcher(methods, pattern string, h ...any) *routerPathMatcher p.params = append(p.params, param) } re = append(re, '$') - reStr := string(re) - p.re = regexp.MustCompile(reStr) + p.re = regexp.MustCompile(string(re)) return p } + +func (g *RouterPathGroup) PatternRegexp(pattern string, h ...any) *RouterPathGroupPattern { + return patternRegexp(pattern, h...) +} diff --git a/modules/web/router_test.go b/modules/web/router_test.go index 21619012ea..1cee2b879b 100644 --- a/modules/web/router_test.go +++ b/modules/web/router_test.go @@ -34,7 +34,7 @@ func TestPathProcessor(t *testing.T) { testProcess := func(pattern, uri string, expectedPathParams map[string]string) { chiCtx := chi.NewRouteContext() chiCtx.RouteMethod = "GET" - p := newRouterPathMatcher("GET", pattern, http.NotFound) + p := newRouterPathMatcher("GET", patternRegexp(pattern), http.NotFound) assert.True(t, p.matchPath(chiCtx, uri), "use pattern %s to process uri %s", pattern, uri) assert.Equal(t, expectedPathParams, chiURLParamsToMap(chiCtx), "use pattern %s to process uri %s", pattern, uri) } @@ -56,18 +56,20 @@ func TestRouter(t *testing.T) { recorder.Body = buff type resultStruct struct { - method string - pathParams map[string]string - handlerMark string + method string + pathParams map[string]string + handlerMarks []string } - var res resultStruct + var res resultStruct h := func(optMark ...string) func(resp http.ResponseWriter, req *http.Request) { mark := util.OptionalArg(optMark, "") return func(resp http.ResponseWriter, req *http.Request) { res.method = req.Method res.pathParams = chiURLParamsToMap(chi.RouteContext(req.Context())) - res.handlerMark = mark + if mark != "" { + res.handlerMarks = append(res.handlerMarks, mark) + } } } @@ -77,6 +79,8 @@ func TestRouter(t *testing.T) { if stop := req.FormValue("stop"); stop != "" && (mark == "" || mark == stop) { h(stop)(resp, req) resp.WriteHeader(http.StatusOK) + } else if mark != "" { + res.handlerMarks = append(res.handlerMarks, mark) } } } @@ -108,7 +112,7 @@ func TestRouter(t *testing.T) { m.Delete("", h()) }) m.PathGroup("/*", func(g *RouterPathGroup) { - g.MatchPath("GET", `/<dir:*>/<file:[a-z]{1,2}>`, stopMark("s2"), h("match-path")) + g.MatchPattern("GET", g.PatternRegexp(`/<dir:*>/<file:[a-z]{1,2}>`, stopMark("s2")), stopMark("s3"), h("match-path")) }, stopMark("s1")) }) }) @@ -126,31 +130,31 @@ func TestRouter(t *testing.T) { } t.Run("RootRouter", func(t *testing.T) { - testRoute(t, "GET /the-user/the-repo/other", resultStruct{method: "GET", handlerMark: "not-found:/"}) + testRoute(t, "GET /the-user/the-repo/other", resultStruct{method: "GET", handlerMarks: []string{"not-found:/"}}) testRoute(t, "GET /the-user/the-repo/pulls", resultStruct{ - method: "GET", - pathParams: map[string]string{"username": "the-user", "reponame": "the-repo", "type": "pulls"}, - handlerMark: "list-issues-b", + method: "GET", + pathParams: map[string]string{"username": "the-user", "reponame": "the-repo", "type": "pulls"}, + handlerMarks: []string{"list-issues-b"}, }) testRoute(t, "GET /the-user/the-repo/issues/123", resultStruct{ - method: "GET", - pathParams: map[string]string{"username": "the-user", "reponame": "the-repo", "type": "issues", "index": "123"}, - handlerMark: "view-issue", + method: "GET", + pathParams: map[string]string{"username": "the-user", "reponame": "the-repo", "type": "issues", "index": "123"}, + handlerMarks: []string{"view-issue"}, }) testRoute(t, "GET /the-user/the-repo/issues/123?stop=hijack", resultStruct{ - method: "GET", - pathParams: map[string]string{"username": "the-user", "reponame": "the-repo", "type": "issues", "index": "123"}, - handlerMark: "hijack", + method: "GET", + pathParams: map[string]string{"username": "the-user", "reponame": "the-repo", "type": "issues", "index": "123"}, + handlerMarks: []string{"hijack"}, }) testRoute(t, "POST /the-user/the-repo/issues/123/update", resultStruct{ - method: "POST", - pathParams: map[string]string{"username": "the-user", "reponame": "the-repo", "index": "123"}, - handlerMark: "update-issue", + method: "POST", + pathParams: map[string]string{"username": "the-user", "reponame": "the-repo", "index": "123"}, + handlerMarks: []string{"update-issue"}, }) }) t.Run("Sub Router", func(t *testing.T) { - testRoute(t, "GET /api/v1/other", resultStruct{method: "GET", handlerMark: "not-found:/api/v1"}) + testRoute(t, "GET /api/v1/other", resultStruct{method: "GET", handlerMarks: []string{"not-found:/api/v1"}}) testRoute(t, "GET /api/v1/repos/the-user/the-repo/branches", resultStruct{ method: "GET", pathParams: map[string]string{"username": "the-user", "reponame": "the-repo"}, @@ -179,31 +183,37 @@ func TestRouter(t *testing.T) { t.Run("MatchPath", func(t *testing.T) { testRoute(t, "GET /api/v1/repos/the-user/the-repo/branches/d1/d2/fn", resultStruct{ - method: "GET", - pathParams: map[string]string{"username": "the-user", "reponame": "the-repo", "*": "d1/d2/fn", "dir": "d1/d2", "file": "fn"}, - handlerMark: "match-path", + method: "GET", + pathParams: map[string]string{"username": "the-user", "reponame": "the-repo", "*": "d1/d2/fn", "dir": "d1/d2", "file": "fn"}, + handlerMarks: []string{"s1", "s2", "s3", "match-path"}, }) testRoute(t, "GET /api/v1/repos/the-user/the-repo/branches/d1%2fd2/fn", resultStruct{ - method: "GET", - pathParams: map[string]string{"username": "the-user", "reponame": "the-repo", "*": "d1%2fd2/fn", "dir": "d1%2fd2", "file": "fn"}, - handlerMark: "match-path", + method: "GET", + pathParams: map[string]string{"username": "the-user", "reponame": "the-repo", "*": "d1%2fd2/fn", "dir": "d1%2fd2", "file": "fn"}, + handlerMarks: []string{"s1", "s2", "s3", "match-path"}, }) testRoute(t, "GET /api/v1/repos/the-user/the-repo/branches/d1/d2/000", resultStruct{ - method: "GET", - pathParams: map[string]string{"reponame": "the-repo", "username": "the-user", "*": "d1/d2/000"}, - handlerMark: "not-found:/api/v1", + method: "GET", + pathParams: map[string]string{"reponame": "the-repo", "username": "the-user", "*": "d1/d2/000"}, + handlerMarks: []string{"s1", "not-found:/api/v1"}, }) testRoute(t, "GET /api/v1/repos/the-user/the-repo/branches/d1/d2/fn?stop=s1", resultStruct{ - method: "GET", - pathParams: map[string]string{"username": "the-user", "reponame": "the-repo", "*": "d1/d2/fn"}, - handlerMark: "s1", + method: "GET", + pathParams: map[string]string{"username": "the-user", "reponame": "the-repo", "*": "d1/d2/fn"}, + handlerMarks: []string{"s1"}, }) testRoute(t, "GET /api/v1/repos/the-user/the-repo/branches/d1/d2/fn?stop=s2", resultStruct{ - method: "GET", - pathParams: map[string]string{"username": "the-user", "reponame": "the-repo", "*": "d1/d2/fn", "dir": "d1/d2", "file": "fn"}, - handlerMark: "s2", + method: "GET", + pathParams: map[string]string{"username": "the-user", "reponame": "the-repo", "*": "d1/d2/fn", "dir": "d1/d2", "file": "fn"}, + handlerMarks: []string{"s1", "s2"}, + }) + + testRoute(t, "GET /api/v1/repos/the-user/the-repo/branches/d1/d2/fn?stop=s3", resultStruct{ + method: "GET", + pathParams: map[string]string{"username": "the-user", "reponame": "the-repo", "*": "d1/d2/fn", "dir": "d1/d2", "file": "fn"}, + handlerMarks: []string{"s1", "s2", "s3"}, }) }) } diff --git a/options/locale/locale_cs-CZ.ini b/options/locale/locale_cs-CZ.ini index 2a3bd3e743..474fe462bd 100644 --- a/options/locale/locale_cs-CZ.ini +++ b/options/locale/locale_cs-CZ.ini @@ -1330,7 +1330,6 @@ editor.update=Aktualizovat %s editor.delete=Odstranit %s editor.patch=PoužÃt záplatu editor.patching=ZáplatovánÃ: -editor.fail_to_apply_patch=Nelze použÃt záplatu „%s“ editor.new_patch=Nová záplata editor.commit_message_desc=PÅ™idat volitelný rozÅ¡ÃÅ™ený popis… editor.signoff_desc=PÅ™idat Signed-off-by podpis pÅ™ispÄ›vatele na konec zprávy o commitu. @@ -1348,8 +1347,6 @@ editor.branch_already_exists=VÄ›tev „%s“ již existuje v tomto repozitáři. editor.directory_is_a_file=Jméno adresáře „%s“ je již použito jako jméno souboru v tomto repozitáři. editor.file_is_a_symlink=`„%s“ je symbolický odkaz. Symbolické odkazy nemohou být upravovány ve webovém editoru` editor.filename_is_a_directory=Jméno souboru „%s“ je již použito jako jméno adresáře v tomto repozitáři. -editor.file_editing_no_longer_exists=Upravovaný soubor „%s“ již nenà souÄástà tohoto repozitáře. -editor.file_deleting_no_longer_exists=Odstraňovaný soubor „%s“ již nenà souÄástà tohoto repozitáře. editor.file_changed_while_editing=Obsah souboru byl zmÄ›nÄ›n od doby, kdy jste zaÄaly s úpravou. <a target="_blank" rel="noopener noreferrer" href="%s">KliknÄ›te zde</a>, abyste je zobrazili, nebo <strong>potvrÄte zmÄ›ny jeÅ¡tÄ› jednou</strong> pro jejich pÅ™epsánÃ. editor.file_already_exists=Soubor „%s“ již existuje v tomto repozitáři. editor.commit_id_not_matching=ID commitu se neshoduje s ID, když jsi zaÄal/a s úpravami. Odevzdat do záplatové vÄ›tve a poté slouÄit. @@ -1357,8 +1354,6 @@ editor.push_out_of_date=Nahránà se zdá být zastaralé. editor.commit_empty_file_header=Odevzdat prázdný soubor editor.commit_empty_file_text=Soubor, který se chystáte odevzdat, je prázdný. PokraÄovat? editor.no_changes_to_show=Žádné zmÄ›ny k zobrazenÃ. -editor.fail_to_update_file=NepodaÅ™ilo se aktualizovat/vytvoÅ™it soubor „%s“. -editor.fail_to_update_file_summary=Chybové hlášenÃ: editor.push_rejected_no_message=ZmÄ›na byla serverem zamÃtnuta bez zprávy. ProsÃm, zkontrolujte háÄky Gitu. editor.push_rejected=ZmÄ›na byla serverem zamÃtnuta. ProsÃm, zkontrolujte háÄky Gitu. editor.push_rejected_summary=Úplná zpráva o odmÃtnutÃ: @@ -1373,6 +1368,7 @@ editor.require_signed_commit=VÄ›tev vyžaduje podepsaný commit editor.cherry_pick=Cherry-pick %s na: editor.revert=Vrátit %s na: + commits.desc=Procházet historii zmÄ›n zdrojového kódu. commits.commits=Commity commits.no_commits=Žádné spoleÄné commity. „%s“ a „%s“ majà zcela odliÅ¡nou historii. @@ -2780,15 +2776,13 @@ settings.visibility.private_shortname=Soukromý settings.update_settings=Upravit nastavenà settings.update_setting_success=Nastavenà organizace bylo upraveno. -settings.change_orgname_prompt=Poznámka: ZmÄ›na názvu organizace také zmÄ›nà adresu URL vašà organizace a uvolnà staré jméno této organizace. -settings.change_orgname_redirect_prompt=Staré jméno bude pÅ™esmÄ›rovávat, dokud nebude znovu obsazeno. + + settings.update_avatar_success=Avatar organizace byl aktualizován. settings.delete=Smazat organizaci settings.delete_account=Smazat tuto organizaci settings.delete_prompt=Organizace bude trvale odstranÄ›na. Tato zmÄ›na <strong>NEMŮŽE</strong> být vrácena! settings.confirm_delete_account=Potvrdit smazánà -settings.delete_org_title=Smazat organizaci -settings.delete_org_desc=Tato organizace bude trvale smazána. PokraÄovat? settings.hooks_desc=PÅ™idat webové háÄky, které budou spouÅ¡tÄ›ny pro <strong>vÅ¡echny repozitáře</strong> v této organizaci. settings.labels_desc=PÅ™idejte Å¡tÃtky, které mohou být použity pro úkoly <strong>vÅ¡ech repositářů</strong> v rámci této organizace. diff --git a/options/locale/locale_de-DE.ini b/options/locale/locale_de-DE.ini index 56dcadd451..ac6503113d 100644 --- a/options/locale/locale_de-DE.ini +++ b/options/locale/locale_de-DE.ini @@ -1352,7 +1352,6 @@ editor.update=%s aktualisiert editor.delete=%s gelöscht editor.patch=Patch anwenden editor.patching=Patche: -editor.fail_to_apply_patch=Patch "%s" nicht anwendbar editor.new_patch=Neuer Patch editor.commit_message_desc=Eine ausführlichere (optionale) Beschreibung hinzufügen… editor.signoff_desc=Am Ende der Commit Nachricht einen Signed-off-by Anhang vom Committer hinzufügen. @@ -1372,8 +1371,6 @@ editor.branch_already_exists=Branch "%s" existiert bereits in diesem Repository. editor.directory_is_a_file=Der Verzeichnisname "%s" wird bereits als Dateiname in diesem Repository verwendet. editor.file_is_a_symlink=`"%s" ist ein symbolischer Link. Symbolische Links können mit dem Web-Editor nicht bearbeitet werden` editor.filename_is_a_directory=Der Dateiname "%s" wird bereits als Verzeichnisname in diesem Repository verwendet. -editor.file_editing_no_longer_exists=Die bearbeitete Datei "%s" existiert nicht mehr in diesem Repository. -editor.file_deleting_no_longer_exists=Die zu löschende Datei "%s" existiert nicht mehr in diesem Repository. editor.file_changed_while_editing=Der Inhalt der Datei hat sich seit dem Beginn der Bearbeitung geändert. <a target="_blank" rel="noopener noreferrer" href="%s">Hier klicken</a>, um die Änderungen anzusehen, oder <strong>Änderungen erneut comitten</strong>, um sie zu überschreiben. editor.file_already_exists=Eine Datei mit dem Namen '%s' existiert bereits in diesem Repository. editor.commit_id_not_matching=Die Commit-ID stimmt nicht mit der ID überein, bei welcher du mit der Bearbeitung begonnen hast. Commite in einen Patch-Branch und merge daraufhin. @@ -1381,8 +1378,6 @@ editor.push_out_of_date=Der Push scheint veraltet zu sein. editor.commit_empty_file_header=Leere Datei committen editor.commit_empty_file_text=Die Datei, die du commiten willst, ist leer. Fortfahren? editor.no_changes_to_show=Keine Änderungen vorhanden. -editor.fail_to_update_file=Fehler beim Aktualisieren/Erstellen der Datei "%s". -editor.fail_to_update_file_summary=Fehlermeldung: editor.push_rejected_no_message=Die Änderung wurde vom Server ohne Nachricht abgelehnt. Bitte überprüfe die Git Hooks. editor.push_rejected=Die Änderung wurde vom Server abgelehnt. Bitte überprüfe die Git Hooks. editor.push_rejected_summary=Vollständige Ablehnungsmeldung: @@ -1397,6 +1392,7 @@ editor.require_signed_commit=Branch erfordert einen signierten Commit editor.cherry_pick=Cherry-Picke %s von: editor.revert=%s zurücksetzen auf: + commits.desc=Durchsuche die Quellcode-Änderungshistorie. commits.commits=Commits commits.no_commits=Keine gemeinsamen Commits. "%s" und "%s" haben vollständig unterschiedliche Historien. @@ -2829,15 +2825,13 @@ settings.visibility.private_shortname=Privat settings.update_settings=Einstellungen speichern settings.update_setting_success=Organisationseinstellungen wurden aktualisiert. -settings.change_orgname_prompt=Hinweis: Das Ändern des Organisationsnamens wird auch die URL deiner Organisation ändern und den alten Namen freigeben. -settings.change_orgname_redirect_prompt=Der alte Name wird weiterleiten, bis er wieder beansprucht wird. + + settings.update_avatar_success=Der Organisationsavatar wurde aktualisiert. settings.delete=Organisation löschen settings.delete_account=Diese Organisation löschen settings.delete_prompt=Die Organisation wird dauerhaft gelöscht. Dies <strong>KANN NICHT</strong> rückgängig gemacht werden! settings.confirm_delete_account=Löschen bestätigen -settings.delete_org_title=Organisation löschen -settings.delete_org_desc=Diese Organisation wird dauerhaft gelöscht. Fortfahren? settings.hooks_desc=Webhooks hinzufügen, die für <strong>alle</strong> Repositories dieser Organisation ausgelöst werden. settings.labels_desc=Labels hinzufügen, die für <strong>alle Repositories</strong> dieser Organisation genutzt werden können. diff --git a/options/locale/locale_el-GR.ini b/options/locale/locale_el-GR.ini index 444fbd26c9..10628e6d41 100644 --- a/options/locale/locale_el-GR.ini +++ b/options/locale/locale_el-GR.ini @@ -1190,7 +1190,6 @@ editor.update=ΕνημÎÏωση %s editor.delete=ΔιαγÏαφή %s editor.patch=ΕφαÏμογή ΔιόÏθωσης editor.patching=ΕπιδιόÏθωση: -editor.fail_to_apply_patch=`Αδυναμία εφαÏμογής της επιδιόÏθωσης "%s"` editor.new_patch=ÎÎα ΔιόÏθωση editor.commit_message_desc=Î Ïοσθήκη Ï€ÏοαιÏετικής εκτενοÏÏ‚ πεÏιγÏαφής… editor.signoff_desc=Î ÏοσθÎστε Îνα Ï€Ïόσθετο Signed-off-by στο Ï„Îλος του μηνÏματος καταγÏαφής της υποβολής. @@ -1208,15 +1207,11 @@ editor.branch_already_exists=Ο κλάδος "%s" υπάÏχει ήδη σε Î±Ï editor.directory_is_a_file=Το όνομα φακÎλου "%s" χÏησιμοποιείται ήδη ως όνομα αÏχείου σε αυτό το αποθετήÏιο. editor.file_is_a_symlink=`Το "%s" είναι συμβολικός σÏνδεσμος. Οι συμβολικοί σÏνδεσμοι δεν μποÏοÏν να επεξεÏγαστοÏν στην ενσωματωμÎνη εφαÏμογή` editor.filename_is_a_directory=Το όνομα αÏχείου "%s" χÏησιμοποιείται ήδη ως όνομα φακÎλου σε αυτό το αποθετήÏιο. -editor.file_editing_no_longer_exists=Το αÏχείο "%s" που επεξεÏγάζεται, δεν υπάÏχει πλÎον σε αυτό το αποθετήÏιο. -editor.file_deleting_no_longer_exists=Το αÏχείο "%s" που διαγÏάφεται, δεν υπάÏχει πλÎον σε αυτό το αποθετήÏιο. editor.file_changed_while_editing=Τα πεÏιεχόμενα του αÏχείου άλλαξαν από τότε που ξεκίνησε η επεξεÏγασία. <a target="_blank" rel="noopener noreferrer" href="%s">Κάντε κλικ εδώ</a> για να τα δείτε ή <strong>Υποβολή Αλλαγών ξανά</strong> για να τα αντικαταστήσετε. editor.file_already_exists=Ένα αÏχείο με το όνομα "%s" υπάÏχει ήδη σε αυτό το αποθετήÏιο. editor.commit_empty_file_header=Υποβολή ενός ÎºÎµÎ½Î¿Ï Î±Ïχείου editor.commit_empty_file_text=Το αÏχείο που Ï€Ïόκειται να υποβληθεί είναι κενό. ΣυνÎχεια; editor.no_changes_to_show=Δεν υπάÏχουν αλλαγÎÏ‚ για εμφάνιση. -editor.fail_to_update_file=Αποτυχία ενημÎÏωσης/δημιουÏγίας του αÏχείου "%s". -editor.fail_to_update_file_summary=Μήνυμα Σφάλματος: editor.push_rejected_no_message=Η αλλαγή αποÏÏίφθηκε από το διακομιστή χωÏίς κάποιο μήνυμα. ΠαÏακαλώ ελÎγξτε τα ΆγκιστÏα Git. editor.push_rejected=Η αλλαγή αποÏÏίφθηκε από τον διακομιστή. ΠαÏακαλώ ελÎγξτε τα ΆγκιστÏα Git. editor.push_rejected_summary=Μήνυμα ΠλήÏους ΑπόÏÏιψης: @@ -1231,6 +1226,7 @@ editor.require_signed_commit=Ο κλάδος απαιτεί υπογεγÏαμμ editor.cherry_pick=Ανθολόγηση (cherry-pic) του %s στο: editor.revert=ΑπόσυÏση του %s στο: + commits.desc=Δείτε το ιστοÏικό αλλαγών του πηγαίου κώδικα. commits.commits=ΥποβολÎÏ‚ commits.no_commits=Δεν υπάÏχουν κοινÎÏ‚ υποβολÎÏ‚. Τα "%s" και "%s" Îχουν εντελώς διαφοÏετικÎÏ‚ ιστοÏίες. @@ -2505,15 +2501,13 @@ settings.visibility.private_shortname=Ιδιωτικός settings.update_settings=ΕνημÎÏωση Ρυθμίσεων settings.update_setting_success=Οι Ïυθμίσεις του οÏÎ³Î±Î½Î¹ÏƒÎ¼Î¿Ï Îχουν ενημεÏωθεί. -settings.change_orgname_prompt=Σημείωση: Η αλλαγή του ονόματος του οÏÎ³Î±Î½Î¹ÏƒÎ¼Î¿Ï Î¸Î± αλλάξει επίσης τη διεÏθυνση URL του οÏÎ³Î±Î½Î¹ÏƒÎ¼Î¿Ï ÏƒÎ±Ï‚ και θα απελευθεÏώσει το παλιό όνομα. -settings.change_orgname_redirect_prompt=Το παλιό όνομα θα ανακατευθÏνει μÎχÏι να διεκδικηθεί. + + settings.update_avatar_success=Η εικόνα του οÏÎ³Î±Î½Î¹ÏƒÎ¼Î¿Ï Îχει ενημεÏωθεί. settings.delete=ΔιαγÏαφή ΟÏÎ³Î±Î½Î¹ÏƒÎ¼Î¿Ï settings.delete_account=ΔιαγÏαφή Î‘Ï…Ï„Î¿Ï Î¤Î¿Ï… ΟÏÎ³Î±Î½Î¹ÏƒÎ¼Î¿Ï settings.delete_prompt=Ο οÏγανισμός θα αφαιÏεθεί οÏιστικά. Αυτό το <strong>ΔΕΠΜΠΟΡΕΙ</strong> να αναιÏεθεί! settings.confirm_delete_account=Επιβεβαίωση ΔιαγÏαφής -settings.delete_org_title=ΔιαγÏαφή ΟÏÎ³Î±Î½Î¹ÏƒÎ¼Î¿Ï -settings.delete_org_desc=Αυτός ο οÏγανισμός θα διαγÏαφεί οÏιστικά. ΣυνÎχεια; settings.hooks_desc=Î Ïοσθήκη webhooks που θα ενεÏγοποιοÏνται για <strong>όλα τα αποθετήÏια</strong> κάτω από αυτό τον οÏγανισμό. settings.labels_desc=Î Ïοσθήκη σημάτων που μποÏοÏν να χÏησιμοποιηθοÏν σε ζητήματα για <strong>όλα τα αποθετήÏια</strong> κάτω από αυτό τον οÏγανισμό. diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini index 8a0e0abf20..0efc9d04c2 100644 --- a/options/locale/locale_en-US.ini +++ b/options/locale/locale_en-US.ini @@ -421,6 +421,7 @@ remember_me.compromised = The login token is not valid anymore which may indicat forgot_password_title= Forgot Password forgot_password = Forgot password? need_account = Need an account? +sign_up_tip = You are registering the first account in the system, which has administrator privileges. Please carefully remember your username and password. If you forget the username or password, please refer to the Gitea documentation to recover the account. sign_up_now = Register now. sign_up_successful = Account was successfully created. Welcome! confirmation_mail_sent_prompt_ex = A new confirmation email has been sent to <b>%s</b>. Please check your inbox within the next %s to complete the registration process. If your registration email address is incorrect, you can sign in again and change it. @@ -1228,7 +1229,7 @@ migrate.migrating_issues = Migrating Issues migrate.migrating_pulls = Migrating Pull Requests migrate.cancel_migrating_title = Cancel Migration migrate.cancel_migrating_confirm = Do you want to cancel this migration? -migrating_status = Migrating status +migration_status = Migration status mirror_from = mirror of forked_from = forked from @@ -1354,7 +1355,7 @@ editor.update = Update %s editor.delete = Delete %s editor.patch = Apply Patch editor.patching = Patching: -editor.fail_to_apply_patch = Unable to apply patch "%s" +editor.fail_to_apply_patch = Unable to apply patch editor.new_patch = New Patch editor.commit_message_desc = Add an optional extended description… editor.signoff_desc = Add a Signed-off-by trailer by the committer at the end of the commit log message. @@ -1374,8 +1375,7 @@ editor.branch_already_exists = Branch "%s" already exists in this repository. editor.directory_is_a_file = Directory name "%s" is already used as a filename in this repository. editor.file_is_a_symlink = `"%s" is a symbolic link. Symbolic links cannot be edited in the web editor` editor.filename_is_a_directory = Filename "%s" is already used as a directory name in this repository. -editor.file_editing_no_longer_exists = The file being edited, "%s", no longer exists in this repository. -editor.file_deleting_no_longer_exists = The file being deleted, "%s", no longer exists in this repository. +editor.file_modifying_no_longer_exists = The file being modified, "%s", no longer exists in this repository. editor.file_changed_while_editing = The file contents have changed since you started editing. <a target="_blank" rel="noopener noreferrer" href="%s">Click here</a> to see them or <strong>Commit Changes again</strong> to overwrite them. editor.file_already_exists = A file named "%s" already exists in this repository. editor.commit_id_not_matching = The Commit ID does not match the ID when you began editing. Commit into a patch branch and then merge. @@ -1383,8 +1383,6 @@ editor.push_out_of_date = The push appears to be out of date. editor.commit_empty_file_header = Commit an empty file editor.commit_empty_file_text = The file you're about to commit is empty. Proceed? editor.no_changes_to_show = There are no changes to show. -editor.fail_to_update_file = Failed to update/create file "%s". -editor.fail_to_update_file_summary = Error Message: editor.push_rejected_no_message = The change was rejected by the server without a message. Please check Git Hooks. editor.push_rejected = The change was rejected by the server. Please check Git Hooks. editor.push_rejected_summary = Full Rejection Message: @@ -1398,6 +1396,15 @@ editor.user_no_push_to_branch = User cannot push to branch editor.require_signed_commit = Branch requires a signed commit editor.cherry_pick = Cherry-pick %s onto: editor.revert = Revert %s onto: +editor.failed_to_commit = Failed to commit changes. +editor.failed_to_commit_summary = Error Message: + +editor.fork_create = Fork Repository to Propose Changes +editor.fork_create_description = You can not edit this repository directly. Instead you can create a fork, make edits and create a pull request. +editor.fork_edit_description = You can not edit this repository directly. The changes will be written to your fork <b>%s</b>, so you can create a pull request. +editor.fork_not_editable = You have forked this repository but your fork is not editable. +editor.fork_failed_to_push_branch = Failed to push branch %s to your repository. +editor.fork_branch_exists = Branch "%s" already exists in your fork, please choose a new branch name. commits.desc = Browse source code change history. commits.commits = Commits @@ -2812,6 +2819,7 @@ team_permission_desc = Permission team_unit_desc = Allow Access to Repository Sections team_unit_disabled = (Disabled) +form.name_been_taken = The organisation name "%s" has already been taken. form.name_reserved = The organization name "%s" is reserved. form.name_pattern_not_allowed = The pattern "%s" is not allowed in an organization name. form.create_org_not_allowed = You are not allowed to create an organization. @@ -2833,15 +2841,28 @@ settings.visibility.private_shortname = Private settings.update_settings = Update Settings settings.update_setting_success = Organization settings have been updated. -settings.change_orgname_prompt = Note: Changing the organization name will also change your organization's URL and free the old name. -settings.change_orgname_redirect_prompt = The old name will redirect until it is claimed. + +settings.rename = Rename Organization +settings.rename_desc = Changing the organization name will also change your organization's URL and free the old name. +settings.rename_success = Organization %[1]s have been renamed to %[2]s successfully. +settings.rename_no_change = Organization name is no change. +settings.rename_new_org_name = New Organization Name +settings.rename_failed = Rename Organization failed because of internal error +settings.rename_notices_1 = This operation <strong>CANNOT</strong> be undone. +settings.rename_notices_2 = The old name will redirect until it is claimed. + settings.update_avatar_success = The organization's avatar has been updated. settings.delete = Delete Organization settings.delete_account = Delete This Organization settings.delete_prompt = The organization will be permanently removed. This <strong>CANNOT</strong> be undone! +settings.name_confirm = Enter the organization name as confirmation: +settings.delete_notices_1 = This operation <strong>CANNOT</strong> be undone. +settings.delete_notices_2 = This operation will permanently delete all the <strong>repositories</strong> of <strong>%s</strong> including code, issues, comments, wiki data and collaborator settings. +settings.delete_notices_3 = This operation will permanently delete all the <strong>packages</strong> of <strong>%s</strong>. +settings.delete_notices_4 = This operation will permanently delete all the <strong>projects</strong> of <strong>%s</strong>. settings.confirm_delete_account = Confirm Deletion -settings.delete_org_title = Delete Organization -settings.delete_org_desc = This organization will be deleted permanently. Continue? +settings.delete_failed = Delete Organization failed because of internal error +settings.delete_successful = Organization <b>%s</b> has been deleted successfully. settings.hooks_desc = Add webhooks which will be triggered for <strong>all repositories</strong> under this organization. settings.labels_desc = Add labels which can be used on issues for <strong>all repositories</strong> under this organization. @@ -3818,6 +3839,7 @@ runs.no_runs = The workflow has no runs yet. runs.empty_commit_message = (empty commit message) runs.expire_log_message = Logs have been purged because they were too old. runs.delete = Delete workflow run +runs.cancel = Cancel workflow run runs.delete.description = Are you sure you want to permanently delete this workflow run? This action cannot be undone. runs.not_done = This workflow run is not done. runs.view_workflow_file = View workflow file diff --git a/options/locale/locale_es-ES.ini b/options/locale/locale_es-ES.ini index 521583395e..3b6c5f3667 100644 --- a/options/locale/locale_es-ES.ini +++ b/options/locale/locale_es-ES.ini @@ -1180,7 +1180,6 @@ editor.update=Actualizar %s editor.delete=Eliminar %s editor.patch=Aplicar parche editor.patching=Parcheando: -editor.fail_to_apply_patch=`No se puede aplicar el parche "%s"` editor.new_patch=Nuevo parche editor.commit_message_desc=Añadir una descripción extendida opcional… editor.signoff_desc=Añadir un trailer firmado por el committer al final del mensaje de registro de confirmación. @@ -1198,15 +1197,11 @@ editor.branch_already_exists=La rama "%s" ya existe en este repositorio. editor.directory_is_a_file=Nombre del directorio "%s" ya se utiliza como nombre de archivo en este repositorio. editor.file_is_a_symlink=`"%s" es un enlace simbólico. Los enlaces simbólicos no se pueden editar en el editor web` editor.filename_is_a_directory=Nombre de archivo "%s" ya se utiliza como nombre de directorio en este repositorio. -editor.file_editing_no_longer_exists=El archivo que se está editando, "%s", ya no existe en este repositorio. -editor.file_deleting_no_longer_exists=El archivo que se está eliminando, "%s", ya no existe en este repositorio. editor.file_changed_while_editing=Desde que comenzó a editar, el contenido del archivo ha sido cambiado. <a target="_blank" rel="noopener noreferrer" href="%s">Haga clic aquÃ</a> para ver qué ha cambiado o <strong>presione confirmar de nuevo</strong> para sobrescribir los cambios. editor.file_already_exists=Ya existe un archivo llamado "%s" en este repositorio. editor.commit_empty_file_header=Commit un archivo vacÃo editor.commit_empty_file_text=El archivo que estás tratando de commit está vacÃo. ¿Proceder? editor.no_changes_to_show=No existen cambios para mostrar. -editor.fail_to_update_file=Error al actualizar/crear el archivo "%s". -editor.fail_to_update_file_summary=Mensaje de error editor.push_rejected_no_message=El cambio fue rechazado por el servidor sin un mensaje. Por favor, compruebe Git Hooks. editor.push_rejected=El cambio fue rechazado por el servidor. Por favor, comprueba los Git Hooks. editor.push_rejected_summary=Mensaje completo de rechazo @@ -1221,6 +1216,7 @@ editor.require_signed_commit=Esta rama requiere un commit firmado editor.cherry_pick=Hacer Cherry-pick %s en: editor.revert=Revertir %s en: + commits.desc=Ver el historial de cambios de código fuente. commits.commits=Commits commits.no_commits=No hay commits en común. "%s" y "%s" tienen historias totalmente diferentes. @@ -2487,15 +2483,13 @@ settings.visibility.private_shortname=Privado settings.update_settings=Actualizar configuración settings.update_setting_success=Configuración de la organización se han actualizado. -settings.change_orgname_prompt=Nota: Cambiar el nombre de la organización también cambiará la URL de su organización y liberará el nombre antiguo. -settings.change_orgname_redirect_prompt=El nombre antiguo se redirigirá hasta que se reclame. + + settings.update_avatar_success=Se ha actualizado el avatar de la organización. settings.delete=Eliminar organización settings.delete_account=Eliminar esta organización settings.delete_prompt=La organización será eliminada permanentemente. ¡Esta acción <strong>NO PUEDE</strong> deshacerse! settings.confirm_delete_account=Confirmar eliminación -settings.delete_org_title=Eliminar organización -settings.delete_org_desc=Esta organización se eliminará permanentemente. ¿Continuar? settings.hooks_desc=Añadir webhooks que serán ejecutados para <strong>todos los repositorios</strong> de esta organización. settings.labels_desc=Añadir etiquetas que pueden ser utilizadas en problemas para <strong>todos los repositorios</strong> bajo esta organización. diff --git a/options/locale/locale_fa-IR.ini b/options/locale/locale_fa-IR.ini index 18abc0f401..e3e5391222 100644 --- a/options/locale/locale_fa-IR.ini +++ b/options/locale/locale_fa-IR.ini @@ -943,13 +943,13 @@ editor.file_changed_while_editing=Ù…ØØªÙˆØ§ÛŒ پرونده تغییر Ù…ÛŒÚ©Ù†Ø editor.commit_empty_file_header=کامیت کردن یک پرونده خالی editor.commit_empty_file_text=ÙØ§ÛŒÙ„ÛŒ Ú©Ù‡ درخواست ارسال دارید خالی است. ادامه بدم? editor.no_changes_to_show=تغییری برای نمایش وجود ندارد. -editor.fail_to_update_file_summary=متن خطا: editor.push_rejected_summary=متن کامل پیام دلیل رد شدن: editor.add_subdir=Ø§ÙØ²ÙˆØ¯Ù† پوشه… editor.no_commit_to_branch=نمی‌توان به طور مستقیم درمورد شاخه نطر داد زیرا: editor.user_no_push_to_branch=کاربر نمیتواند به شاخه ارسال کند editor.require_signed_commit=شاخه یک کامیت امضا شده لازم دارد + commits.desc=تاریخچه تغییرات کد منبع را مرور کنید. commits.commits=کامیت‌ها commits.nothing_to_compare=این شاخه ها برابرند. @@ -1920,14 +1920,13 @@ settings.visibility.private_shortname=پوشیده settings.update_settings=به‌ روزرسانی تنظیمات settings.update_setting_success=تنظیمات این سازمان به‌روز شد. -settings.change_orgname_redirect_prompt=نام قدیمی تا زمانی Ú©Ù‡ ادعا شود تغییر مسیر Ù…ÛŒ دهد. + + settings.update_avatar_success=آواتار این سازمان به‌روز شد. settings.delete=ØØ°Ù سازمان settings.delete_account=ØØ°Ù این سازمان settings.delete_prompt=سازمان برای همیشه ØØ°Ù خواهد شد. این قابل برگشت <strong>نخواهد بود</strong>! settings.confirm_delete_account=تاییدیه ØØ°Ù -settings.delete_org_title=ØØ°Ù سازمان -settings.delete_org_desc=سازمان برای همیشه ØØ°Ù خواهد شد. آیا همچنان ادامه می‌دهید؟ settings.hooks_desc=Ø§ÙØ²ÙˆØ¯Ù† webhook های Ú©Ù‡ برای<strong> تمام مخازن</strong> این سازمان اجرا میشود. settings.labels_desc=تگ هایی را اضاÙÙ‡ کنید Ú©Ù‡ می‌توانند برای مشکلات <strong>همه مخازن</strong> ØªØØª این سازمان Ø§Ø³ØªÙØ§Ø¯Ù‡ شوند. diff --git a/options/locale/locale_fi-FI.ini b/options/locale/locale_fi-FI.ini index b925d6f43a..75327f4d87 100644 --- a/options/locale/locale_fi-FI.ini +++ b/options/locale/locale_fi-FI.ini @@ -764,6 +764,7 @@ editor.no_changes_to_show=Ei muutoksia näytettäväksi. editor.add_subdir=Lisää hakemisto… editor.require_signed_commit=Haara vaatii vahvistetun commitin + commits.commits=Commitit commits.nothing_to_compare=Nämä haarat vastaavat toisiaan. commits.search_all=Kaikki haarat @@ -1318,11 +1319,12 @@ settings.visibility.private=Yksityinen (näkyvä vain organisaation jäsenille) settings.visibility.private_shortname=Yksityinen settings.update_settings=Päivitä asetukset + + settings.delete=Poista organisaatio settings.delete_account=Poista tämä organisaatio settings.delete_prompt=Organisaatio poistetaan pysyvästi, ja tätä <strong>EI VOI</strong> peruuttaa myöhemmin! settings.confirm_delete_account=Vahvista poisto -settings.delete_org_title=Poista organisaatio settings.hooks_desc=Lisää webkoukkuja, jotka suoritetaan <strong>kaikissa repoissa</strong> tässä organisaatiossa. diff --git a/options/locale/locale_fr-FR.ini b/options/locale/locale_fr-FR.ini index 5dd918fb91..40e7a5699a 100644 --- a/options/locale/locale_fr-FR.ini +++ b/options/locale/locale_fr-FR.ini @@ -1354,7 +1354,6 @@ editor.update=Actualiser %s editor.delete=Supprimer %s editor.patch=Appliquer le correctif editor.patching=Correction: -editor.fail_to_apply_patch=`Impossible d'appliquer le correctif "%s"` editor.new_patch=Nouveau correctif editor.commit_message_desc=Ajouter une description détaillée facultative… editor.signoff_desc=Créditer l'auteur "Signed-off-by:" en pied de révision. @@ -1374,8 +1373,6 @@ editor.branch_already_exists=La branche "%s" existe déjà dans ce dépôt. editor.directory_is_a_file=Le nom de dossier "%s" est déjà utilisé comme nom de fichier dans ce dépôt. editor.file_is_a_symlink=`« %s » est un lien symbolique. Ce type de fichiers ne peut être modifié dans l'éditeur web.` editor.filename_is_a_directory=« %s » est déjà utilisé comme nom de dossier dans ce dépôt. -editor.file_editing_no_longer_exists=Impossible de modifier le fichier « %s » car il n’existe plus dans ce dépôt. -editor.file_deleting_no_longer_exists=Impossible de supprimer le fichier « %s » car il n’existe plus dans ce dépôt. editor.file_changed_while_editing=Le contenu du fichier a changé depuis que vous avez commencé à éditer. <a target="_blank" rel="noopener noreferrer" href="%s">Cliquez ici</a> pour voir les changements ou <strong>soumettez de nouveau</strong> pour les écraser. editor.file_already_exists=Un fichier nommé "%s" existe déjà dans ce dépôt. editor.commit_id_not_matching=L’ID de la révision ne correspond pas à l’ID lorsque vous avez commencé à éditer. Faites une révision dans une branche de correctif puis fusionnez. @@ -1383,8 +1380,6 @@ editor.push_out_of_date=Cet envoi semble être obsolète. editor.commit_empty_file_header=Réviser un fichier vide editor.commit_empty_file_text=Le fichier que vous allez réviser est vide. Continuer ? editor.no_changes_to_show=Il n’y a aucune modification à afficher. -editor.fail_to_update_file=Impossible de mettre à jour/créer le fichier "%s". -editor.fail_to_update_file_summary=Message d'erreur : editor.push_rejected_no_message=La modification a été rejetée par le serveur sans message. Veuillez vérifier les Git Hooks. editor.push_rejected=La modification a été rejetée par le serveur. Veuillez vérifier vos Git Hooks. editor.push_rejected_summary=Message de rejet complet : @@ -1399,6 +1394,7 @@ editor.require_signed_commit=Cette branche nécessite une révision signée editor.cherry_pick=Picorer %s vers: editor.revert=Rétablir %s sur: + commits.desc=Naviguer dans l'historique des modifications. commits.commits=Révisions commits.no_commits=Pas de révisions en commun. "%s" et "%s" ont des historiques entièrement différents. @@ -1654,6 +1650,7 @@ issues.save=Enregistrer issues.label_title=Nom du label issues.label_description=Description du label issues.label_color=Couleur du label +issues.label_color_invalid=Couleur invalide issues.label_exclusive=Exclusif issues.label_archive=Archivé issues.label_archived_filter=Afficher les labels archivés @@ -2830,15 +2827,13 @@ settings.visibility.private_shortname=Privé settings.update_settings=Appliquer les paramètres settings.update_setting_success=Les paramètres de l'organisation ont été mis à jour. -settings.change_orgname_prompt=Remarque : Changer le nom de l'organisation changera également l'URL de votre organisation et libèrera l'ancien nom. -settings.change_orgname_redirect_prompt=L'ancien nom d'utilisateur redirigera jusqu'à ce qu'il soit réclamé. + + settings.update_avatar_success=L'avatar de l'organisation a été mis à jour. settings.delete=Supprimer l'organisation settings.delete_account=Supprimer cette organisation settings.delete_prompt=Cette organisation sera supprimée définitivement. Cette action est <strong>IRRÉVERSIBLE</strong> ! settings.confirm_delete_account=Confirmer la suppression -settings.delete_org_title=Supprimer l'organisation -settings.delete_org_desc=Cette organisation sera supprimée définitivement. Voulez-vous continuer ? settings.hooks_desc=Vous pouvez ajouter des webhooks qui seront activés pour <strong>tous les dépôts</strong> de cette organisation. settings.labels_desc=Ajoute des labels qui peuvent être utilisés sur les tickets pour <strong>tous les dépôts</strong> de cette organisation. diff --git a/options/locale/locale_ga-IE.ini b/options/locale/locale_ga-IE.ini index 64f893f03c..fab6c934bc 100644 --- a/options/locale/locale_ga-IE.ini +++ b/options/locale/locale_ga-IE.ini @@ -421,6 +421,7 @@ remember_me.compromised=NÃl an comhartha logála isteach bailà nÃos mó a d'f forgot_password_title=Dearmad ar an bPasfhocal forgot_password=Dearmad ar an bPasfhocal? need_account=An bhfuil cuntas ag teastáil uait? +sign_up_tip=Tá tú ag clárú an chéad chuntais sa chóras, a bhfuil pribhléidà riarthóra aige. Cuimhnigh go cúramach ar d’ainm úsáideora agus do phasfhocal. Má dhéanann tú dearmad ar an ainm úsáideora nó ar an pasfhocal, féach ar dhoiciméadacht Gitea le do thoil chun an cuntas a aisghabháil. sign_up_now=Cláraigh anois. sign_up_successful=CruthaÃodh cuntas go rathúil. Fáilte romhat! confirmation_mail_sent_prompt_ex=Tá rÃomhphost dearbhaithe nua seolta chuig <b>%s</b>. Seiceáil do bhosca isteach laistigh den chéad %s eile chun an próiseas clárúcháin a chur i gcrÃch. Má tá do sheoladh rÃomhphoist clárúcháin mÃcheart, is féidir leat sÃniú isteach arÃs agus é a athrú. @@ -1354,7 +1355,6 @@ editor.update=Nuashonraigh %s editor.delete=Scrios %s editor.patch=Cuir paiste i bhfeidh editor.patching=Paisteáil: -editor.fail_to_apply_patch=Nà féidir paiste "%s" a chur i bhfeidhm editor.new_patch=Paiste Nua editor.commit_message_desc=Cuir cur sÃos leathnaithe roghnach leis… editor.signoff_desc=Cuir leantóir sÃnithe ag an gcoiteoir ag deireadh na teachtaireachta logála tiomanta. @@ -1374,8 +1374,7 @@ editor.branch_already_exists=Tá brainse "%s" ann cheana féin sa stóras seo. editor.directory_is_a_file=Úsáidtear ainm eolaire "%s" cheana féin mar ainm comhaid sa stóras seo. editor.file_is_a_symlink=Is nasc siombalach é "%s". Nà féidir naisc shiombalacha a chur in eagar san eagarthóir gréasáin editor.filename_is_a_directory=Úsáidtear ainm comhaid "%s" cheana féin mar ainm eolaire sa stóras seo. -editor.file_editing_no_longer_exists=NÃl an comhad atá á chur in eagar, "%s", ann sa stóras seo a thuilleadh. -editor.file_deleting_no_longer_exists=NÃl an comhad atá á scriosadh, "%s", ann sa stóras seo a thuilleadh. +editor.file_modifying_no_longer_exists=NÃl an comhad atá á mhodhnú, "%s", sa stóras seo a thuilleadh. editor.file_changed_while_editing=Tá athrú tagtha ar ábhar an chomhad ó thosaigh tú ag eagarthóireacht <a target="_blank" rel="noopener noreferrer" href="%s">Cliceáil anseo</a> chun iad a fheiceáil nó Athru <strong>ithe a Tiomantas arÃs</strong> chun iad a fhorscrÃobh. editor.file_already_exists=Tá comhad darb ainm "%s" ann cheana féin sa stóras seo. editor.commit_id_not_matching=Nà mheaitseálann an ID Tiomanta leis an ID nuair a thosaigh tú ag eagarthóireacht. Tiomanta isteach i mbrainse paiste agus ansin cumaisc. @@ -1383,8 +1382,6 @@ editor.push_out_of_date=Is cosúil go bhfuil an brú as dáta. editor.commit_empty_file_header=Tiomantas comhad folamh editor.commit_empty_file_text=Tá an comhad atá tú ar tà tiomantas folamh. Ar aghaidh? editor.no_changes_to_show=NÃl aon athruithe le taispeáint. -editor.fail_to_update_file=Theip ar nuashonrú/cruthú comhad "%s". -editor.fail_to_update_file_summary=Teachtaireacht Earráide: editor.push_rejected_no_message=Dhiúltaigh an freastalaà an t-athrú gan teachtaireacht. Seiceáil Git Hooks le do thoil. editor.push_rejected=Dhiúltaigh an freastalaà an t-athrú. Seiceáil Git Hooks le do thoil. editor.push_rejected_summary=Teachtaireacht Diúltaithe Iomlán: @@ -1398,6 +1395,9 @@ editor.user_no_push_to_branch=Nà féidir leis an úsáideoir brúigh go dtà an editor.require_signed_commit=ÉilÃonn an Brainse tiomantas sÃnithe editor.cherry_pick=Roghnaigh silÃnà %s ar: editor.revert=Fill %s ar: +editor.failed_to_commit=Theip ar athruithe a chur i bhfeidhm. +editor.failed_to_commit_summary=Teachtaireacht Earráide: + commits.desc=Brabhsáil stair athraithe cód foinse. commits.commits=Tiomáintà @@ -2403,6 +2403,8 @@ settings.event_pull_request_review_request_desc=Tarraing athbhreithniú iarratai settings.event_pull_request_approvals=Ceaduithe Iarratais Tarraing settings.event_pull_request_merge=Cumaisc Iarratas Tarraing settings.event_header_workflow=Imeachtaà Sreabhadh Oibre +settings.event_workflow_run=Rith Sreabhadh Oibre +settings.event_workflow_run_desc=Tá rith Sreabhadh Oibre GnÃomhartha Gitea sa scuaine, ag fanacht, ar siúl, nó crÃochnaithe. settings.event_workflow_job=Poist Sreabhadh Oibre settings.event_workflow_job_desc=Gitea Actions Sreabhadh oibre post ciúáilte, ag fanacht, ar siúl, nó crÃochnaithe. settings.event_package=Pacáiste @@ -2810,6 +2812,7 @@ team_permission_desc=Cead team_unit_desc=Ceadaigh Rochtain ar Rannóga Stóras team_unit_disabled=(DÃchumasaithe) +form.name_been_taken=Tá ainm na heagraÃochta "%s" tógtha cheana féin. form.name_reserved=Tá an t-ainm eagraÃochta "%s" curtha in áirithe. form.name_pattern_not_allowed=Nà cheadaÃtear an patrún "%s" in ainm eagraÃochta. form.create_org_not_allowed=NÃl cead agat eagraÃocht a chruthú. @@ -2831,15 +2834,28 @@ settings.visibility.private_shortname=PrÃobháideach settings.update_settings=Nuashonrú Socruithe settings.update_setting_success=NuashonraÃodh socruithe eagraÃochta. -settings.change_orgname_prompt=Nóta: Athróidh ainm na heagraÃochta ag athrú URL d'eagraÃochta agus saorfar an sean-ainm. -settings.change_orgname_redirect_prompt=Déanfaidh an sean-ainm a atreorú go dtà go n-éilÃtear é. + +settings.rename=Athainmnigh an EagraÃocht +settings.rename_desc=Má athraÃonn tú ainm na heagraÃochta, athrófar URL d’eagraÃochta freisin agus saorfar an seanainm. +settings.rename_success=AthainmnÃodh an eagraÃocht %[1]s go %[2]s go rathúil. +settings.rename_no_change=NÃl aon athrú ar ainm na heagraÃochta. +settings.rename_new_org_name=Ainm Nua na hEagraÃochta +settings.rename_failed=Theip ar athainmniú na hEagraÃochta mar gheall ar earráid inmheánach +settings.rename_notices_1=Nà <strong>FÉIDIR</strong> an oibrÃocht seo a chealú. +settings.rename_notices_2=Déanfar an seanainm a atreorú go dtà go n-éileofar é. + settings.update_avatar_success=NuashonraÃodh avatar na heagraÃochta. settings.delete=Scrios EagraÃocht settings.delete_account=Scrios an EagraÃocht seo settings.delete_prompt=Bainfear an eagraÃocht go buan. <strong>Nà FÉIDIR</strong> é seo a chealú! +settings.name_confirm=Cuir isteach ainm na heagraÃochta mar dheimhniú: +settings.delete_notices_1=Nà <strong>FÉIDIR</strong> an oibrÃocht seo a chealú. +settings.delete_notices_2=Scriosfaidh an oibrÃocht seo go buan gach <strong>stórais</strong> de chuid <strong>%s</strong>, lena n-áirÃtear cód, saincheisteanna, tuairimÃ, sonraà vicà agus socruithe comhoibritheora. +settings.delete_notices_3=Scriosfaidh an oibrÃocht seo gach <strong>pacáiste</strong> de chuid <strong>%s</strong> go buan. +settings.delete_notices_4=Scriosfaidh an oibrÃocht seo gach <strong>tionscadal</strong> de chuid <strong>%s</strong> go buan. settings.confirm_delete_account=Deimhnigh scriosadh -settings.delete_org_title=Scrios EagraÃocht -settings.delete_org_desc=Scriosfar an eagraÃocht seo go buan. Lean ar aghaidh? +settings.delete_failed=Theip ar Scriosadh na hEagraÃochta mar gheall ar earráid inmheánach +settings.delete_successful=Scriosadh an eagraÃocht <b>%s</b> go rathúil. settings.hooks_desc=Cuir crúcaà gréasán in leis a spreagfar do <strong>gach stóras</strong> faoin eagraÃocht seo. settings.labels_desc=Cuir lipéid leis ar féidir iad a úsáid ar shaincheisteanna do <strong>gach stóras</strong> faoin eagraÃocht seo. diff --git a/options/locale/locale_hu-HU.ini b/options/locale/locale_hu-HU.ini index ebc6d5c801..e823b325a5 100644 --- a/options/locale/locale_hu-HU.ini +++ b/options/locale/locale_hu-HU.ini @@ -711,6 +711,7 @@ editor.commit_empty_file_header=Egy üres fájl commitolása editor.no_changes_to_show=Nincsen megjelenÃthetÅ‘ változás. editor.add_subdir=Mappa hozzáadása… + commits.commits=Commit-ok commits.search_all=Minden ág commits.author=SzerzÅ‘ @@ -1174,13 +1175,13 @@ settings.visibility.private_shortname=Privát settings.update_settings=BeállÃtások frissÃtése settings.update_setting_success=A szervezet beállÃtásai frissültek. + + settings.update_avatar_success=A szervezet avatarja frissÃtve. settings.delete=Szervezet törlése settings.delete_account=A szervezet törlése settings.delete_prompt=A szervezet véglegesen el lesz távolÃtva. <strong>NEM</strong> vonható vissza! settings.confirm_delete_account=Törlés megerÅ‘sÃtése -settings.delete_org_title=Szervezet törlése -settings.delete_org_desc=Ez a szervezet véglegesen törölve lesz. Folytatható? settings.hooks_desc=Webhook hozzáadása a szervezet <strong>összes tárolójához</strong>. diff --git a/options/locale/locale_id-ID.ini b/options/locale/locale_id-ID.ini index 54b0499d96..38f65c74ff 100644 --- a/options/locale/locale_id-ID.ini +++ b/options/locale/locale_id-ID.ini @@ -717,6 +717,7 @@ editor.new_branch_name_desc=Nama branch baru… editor.cancel=Membatalkan editor.no_changes_to_show=Tidak ada perubahan untuk ditampilkan. + commits.commits=Melakukan commits.author=Penulis commits.message=Pesan @@ -1055,10 +1056,11 @@ settings.visibility.private_shortname=Pribadi settings.update_settings=Perbarui Setelan settings.update_setting_success=Pengaturan organisasi telah diperbarui. + + settings.delete=Menghapus Organisasi settings.delete_account=Menghapus Organisasi Ini settings.confirm_delete_account=Konfirmasi Penghapusan -settings.delete_org_title=Menghapus Organisasi settings.hooks_desc=Tambahkan webhooks yang akan dipicu untuk <strong>semua repositori</strong> di bawah organisasi ini. diff --git a/options/locale/locale_is-IS.ini b/options/locale/locale_is-IS.ini index 42ecfabe22..200635bae4 100644 --- a/options/locale/locale_is-IS.ini +++ b/options/locale/locale_is-IS.ini @@ -689,7 +689,7 @@ editor.create_new_branch=Búðu til <strong>nýja grein</strong> og sameiningarb editor.create_new_branch_np=Búðu til <strong>nýja grein</strong> fyrir þetta framlag. editor.new_branch_name_desc=Heiti nýjar greinar… editor.cancel=Hætta við -editor.fail_to_update_file_summary=Villuskilaboð: + commits.commits=Framlög commits.author=Höfundur @@ -1118,6 +1118,8 @@ settings.visibility.private_shortname=Einka settings.update_settings=Uppfæra Stillingar + + members.private=Faldir members.owner=Eigandi members.member=Meðlimur diff --git a/options/locale/locale_it-IT.ini b/options/locale/locale_it-IT.ini index 9cc257029b..eec0439e8b 100644 --- a/options/locale/locale_it-IT.ini +++ b/options/locale/locale_it-IT.ini @@ -1014,7 +1014,6 @@ editor.file_changed_while_editing=I contenuti di questo file hanno subito dei ca editor.commit_empty_file_header=Commit di un file vuoto editor.commit_empty_file_text=Il file che stai per effettuare il commit è vuoto. Procedere? editor.no_changes_to_show=Non ci sono cambiamenti da mostrare. -editor.fail_to_update_file_summary=Messaggio d'errore: editor.push_rejected_no_message=La modifica è stata rifiutata dal server senza un messaggio. Controlla Git Hooks. editor.push_rejected=La modifica è stata rifiutata dal server. Controlla Git Hooks. editor.push_rejected_summary=Messaggio Di Rifiuto Completo: @@ -1025,6 +1024,7 @@ editor.require_signed_commit=Il branch richiede un commit firmato editor.cherry_pick=Cherry-pick %s suto: editor.revert=Ripristina %s su: + commits.desc=Sfoglia la cronologia di modifiche del codice rogente. commits.commits=Commit commits.nothing_to_compare=Questi rami sono uguali. @@ -2078,14 +2078,13 @@ settings.visibility.private_shortname=Privato settings.update_settings=Aggiorna Impostazioni settings.update_setting_success=Le impostazioni dell'organizzazione sono state aggiornate. -settings.change_orgname_redirect_prompt=Il vecchio nome reindirizzerà fino a quando non sarà richiesto. + + settings.update_avatar_success=L'avatar dell'organizzazione è stato aggiornato. settings.delete=Elimina organizzazione settings.delete_account=Elimina questa organizzazione settings.delete_prompt=L'organizzazione verrà rimossa definitivamente. Questa operazione <strong>NON PUÃ’</strong> essere annullata! settings.confirm_delete_account=Conferma Eliminazione -settings.delete_org_title=Elimina organizzazione -settings.delete_org_desc=Questa organizzazione verrà eliminata definitivamente. Continuare? settings.hooks_desc=Aggiungi i webhooks che verranno attivati per <strong>tutti i repository</strong> sotto questa organizzazione. settings.labels_desc=Aggiungi i webhooks che verranno attivati per <strong>tutti i repository</strong> sotto questa organizzazione. diff --git a/options/locale/locale_ja-JP.ini b/options/locale/locale_ja-JP.ini index 2934384052..b4b9e0d8a6 100644 --- a/options/locale/locale_ja-JP.ini +++ b/options/locale/locale_ja-JP.ini @@ -421,6 +421,7 @@ remember_me.compromised=ãƒã‚°ã‚¤ãƒ³ãƒˆãƒ¼ã‚¯ãƒ³ã¯ã‚‚ã†æœ‰åйã§ã¯ãªãã€ã‚ forgot_password_title=パスワードを忘れ㟠forgot_password=パスワードをãŠå¿˜ã‚Œã§ã™ã‹ï¼Ÿ need_account=アカウントãŒå¿…è¦ã§ã™ã‹ï¼Ÿ +sign_up_tip=管ç†è€…権é™ã‚’æŒã¤ã€ã“ã®ã‚·ã‚¹ãƒ†ãƒ ã®æœ€åˆã®ã‚¢ã‚«ã‚¦ãƒ³ãƒˆã‚’登録ã—よã†ã¨ã—ã¦ã„ã¾ã™ã€‚ ユーザーåã¨ãƒ‘スワードをよã覚ãˆã¦ãŠã„ã¦ãã ã•ã„。 ユーザーåã¾ãŸã¯ãƒ‘スワードを忘れãŸå ´åˆã¯ã€Giteaã®ãƒ‰ã‚ュメントをå‚ç…§ã—ã¦ã‚¢ã‚«ã‚¦ãƒ³ãƒˆã‚’復元ã—ã¦ãã ã•ã„。 sign_up_now=登録ã¯ã“ã¡ã‚‰ã€‚ sign_up_successful=アカウントã¯ç„¡äº‹ã«ä½œæˆã•れã¾ã—ãŸã€‚よã†ã“ã! confirmation_mail_sent_prompt_ex=æ–°ã—ã„確èªãƒ¡ãƒ¼ãƒ«ã‚’ <b>%s</b> ã«é€ä¿¡ã—ã¾ã—ãŸã€‚ %s以内ã«ãƒ¡ãƒ¼ãƒ«ãƒœãƒƒã‚¯ã‚¹ã‚’確èªã—ã€ç™»éŒ²æ‰‹ç¶šãを完了ã—ã¦ãã ã•ã„。 登録メールアドレスãŒé–“é•ã£ã¦ã„ã‚‹å ´åˆã¯ã€ã‚‚ã†ã„ã¡ã©ã‚µã‚¤ãƒ³ã‚¤ãƒ³ã™ã‚‹ã¨å¤‰æ›´ã™ã‚‹ã“ã¨ãŒã§ãã¾ã™ã€‚ @@ -1332,7 +1333,9 @@ editor.upload_file=ファイルをアップãƒãƒ¼ãƒ‰ editor.edit_file=ファイルを編集 editor.preview_changes=変更をプレビュー editor.cannot_edit_lfs_files=LFSã®ãƒ•ァイルã¯Webインターフェースã§ç·¨é›†ã§ãã¾ã›ã‚“。 +editor.cannot_edit_too_large_file=ã“ã®ãƒ•ァイルã¯å¤§ãã™ãŽã‚‹ãŸã‚ã€ç·¨é›†ã§ãã¾ã›ã‚“。 editor.cannot_edit_non_text_files=ãƒã‚¤ãƒŠãƒªãƒ•ァイルã¯Webインターフェースã§ç·¨é›†ã§ãã¾ã›ã‚“。 +editor.file_not_editable_hint=åå‰ã®å¤‰æ›´ã‚„移動ã¯å¯èƒ½ã§ã™ã€‚ editor.edit_this_file=ファイルを編集 editor.this_file_locked=ファイルã¯ãƒãƒƒã‚¯ã•れã¦ã„ã¾ã™ editor.must_be_on_a_branch=ã“ã®ãƒ•ァイルを変更ã—ãŸã‚Šå¤‰æ›´ã®ææ¡ˆã‚’ã™ã‚‹ã«ã¯ã€ãƒ–ランãƒä¸Šã«ã„ã‚‹å¿…è¦ãŒã‚りã¾ã™ã€‚ @@ -1352,7 +1355,7 @@ editor.update=%s ã‚’æ›´æ–° editor.delete=%s を削除 editor.patch=パッãƒã®é©ç”¨ editor.patching=パッãƒ: -editor.fail_to_apply_patch=`パッãƒã‚’é©ç”¨ã§ãã¾ã›ã‚“ "%s"` +editor.fail_to_apply_patch=パッãƒã‚’é©ç”¨ã§ãã¾ã›ã‚“ editor.new_patch=æ–°ã—ã„パッムeditor.commit_message_desc=詳細ãªèª¬æ˜Žã‚’è¿½åŠ â€¦ editor.signoff_desc=コミットãƒã‚°ãƒ¡ãƒƒã‚»ãƒ¼ã‚¸ã®æœ€å¾Œã«ã‚³ãƒŸãƒƒã‚¿ãƒ¼ã® Signed-off-by è¡Œã‚’è¿½åŠ @@ -1372,8 +1375,7 @@ editor.branch_already_exists=ブランム"%s" ã¯ã€ã“ã®ãƒªãƒã‚¸ãƒˆãƒªã«æ—¢ editor.directory_is_a_file=ディレクトリå "%s" ã¯ã™ã§ã«ãƒªãƒã‚¸ãƒˆãƒªå†…ã®ãƒ•ァイルã§ä½¿ç”¨ã•れã¦ã„ã¾ã™ã€‚ editor.file_is_a_symlink=`"%s" ã¯ã‚·ãƒ³ãƒœãƒªãƒƒã‚¯ãƒªãƒ³ã‚¯ã§ã™ã€‚ シンボリックリンクã¯Webエディターã§ç·¨é›†ã§ãã¾ã›ã‚“。` editor.filename_is_a_directory=ファイルå "%s" ã¯ã€ã“ã®ãƒªãƒã‚¸ãƒˆãƒªä¸Šã§ãƒ‡ã‚£ãƒ¬ã‚¯ãƒˆãƒªåã¨ã—ã¦ã™ã§ã«ä½¿ç”¨ã•れã¦ã„ã¾ã™ã€‚ -editor.file_editing_no_longer_exists=編集ä¸ã®ãƒ•ァイル "%s" ãŒã€ã‚‚ã†ãƒªãƒã‚¸ãƒˆãƒªå†…ã«ã‚りã¾ã›ã‚“。 -editor.file_deleting_no_longer_exists=削除ã—よã†ã¨ã—ãŸãƒ•ァイル "%s" ãŒã€ã™ã§ã«ãƒªãƒã‚¸ãƒˆãƒªå†…ã«ã‚りã¾ã›ã‚“。 +editor.file_modifying_no_longer_exists=ä¿®æ£ä¸ã®ãƒ•ァイル "%s" ãŒã€ã™ã§ã«ãƒªãƒã‚¸ãƒˆãƒªå†…ã«ã‚りã¾ã›ã‚“。 editor.file_changed_while_editing=ã‚ãªãŸãŒç·¨é›†ã‚’é–‹å§‹ã—ãŸã‚ã¨ã€ãƒ•ァイルã®å†…容ãŒå¤‰æ›´ã•れã¾ã—ãŸã€‚ <a target="_blank" rel="noopener noreferrer" href="%s">ã“ã“をクリック</a>ã—ã¦ä½•ãŒå¤‰æ›´ã•れãŸã‹ç¢ºèªã™ã‚‹ã‹ã€<strong>ã‚‚ã†ä¸€åº¦"変更をコミット"をクリック</strong>ã—ã¦ä¸Šæ›¸ãã—ã¾ã™ã€‚ editor.file_already_exists=ファイル "%s" ã¯ã€ã“ã®ãƒªãƒã‚¸ãƒˆãƒªã«æ—¢ã«å˜åœ¨ã—ã¾ã™ã€‚ editor.commit_id_not_matching=コミットIDãŒç·¨é›†ã‚’é–‹å§‹ã—ãŸã¨ãã®IDã¨ä¸€è‡´ã—ã¾ã›ã‚“。 パッãƒç”¨ã®ãƒ–ランãƒã«ã‚³ãƒŸãƒƒãƒˆã—ãŸã‚ã¨ãƒžãƒ¼ã‚¸ã—ã¦ãã ã•ã„。 @@ -1381,8 +1383,6 @@ editor.push_out_of_date=ã“ã®ãƒ—ãƒƒã‚·ãƒ¥ã¯æœ€æ–°ã§ã¯ãªã„よã†ã§ã™ã€‚ editor.commit_empty_file_header=空ファイルã®ã‚³ãƒŸãƒƒãƒˆ editor.commit_empty_file_text=コミットã—よã†ã¨ã—ã¦ã„るファイルã¯ç©ºã§ã™ã€‚ 続行ã—ã¾ã™ã‹ï¼Ÿ editor.no_changes_to_show=表示ã™ã‚‹å¤‰æ›´ç®‡æ‰€ã¯ã‚りã¾ã›ã‚“。 -editor.fail_to_update_file=ファイル "%s" を作æˆã¾ãŸã¯å¤‰æ›´ã§ãã¾ã›ã‚“ã§ã—ãŸã€‚ -editor.fail_to_update_file_summary=エラーメッセージ: editor.push_rejected_no_message=サーãƒãƒ¼ãŒãƒ¡ãƒƒã‚»ãƒ¼ã‚¸ã‚’出ã•ãšã«å¤‰æ›´ã‚’æ‹’å¦ã—ã¾ã—ãŸã€‚ Git フックを確èªã—ã¦ãã ã•ã„。 editor.push_rejected=サーãƒãƒ¼ãŒå¤‰æ›´ã‚’æ‹’å¦ã—ã¾ã—ãŸã€‚ Gitフックを確èªã—ã¦ãã ã•ã„。 editor.push_rejected_summary=æ‹’å¦ãƒ¡ãƒƒã‚»ãƒ¼ã‚¸å…¨ä½“: @@ -1396,6 +1396,9 @@ editor.user_no_push_to_branch=ユーザーã¯ãƒ–ランãƒã«ãƒ—ッシュã§ãã editor.require_signed_commit=ブランãƒã§ã¯ç½²åã•れãŸã‚³ãƒŸãƒƒãƒˆãŒå¿…é ˆã§ã™ editor.cherry_pick=ãƒã‚§ãƒªãƒ¼ãƒ”ック %s: editor.revert=リãƒãƒ¼ãƒˆ %s: +editor.failed_to_commit=変更ã®ã‚³ãƒŸãƒƒãƒˆã«å¤±æ•—ã—ã¾ã—ãŸã€‚ +editor.failed_to_commit_summary=エラーメッセージ: + commits.desc=ソースコードã®å¤‰æ›´å±¥æ´ã‚’å‚ç…§ã—ã¾ã™ã€‚ commits.commits=コミット @@ -1560,6 +1563,7 @@ issues.filter_user_placeholder=ユーザーを検索 issues.filter_user_no_select=ã™ã¹ã¦ã®ãƒ¦ãƒ¼ã‚¶ãƒ¼ issues.filter_type=タイプ issues.filter_type.all_issues=ã™ã¹ã¦ã®ã‚¤ã‚·ãƒ¥ãƒ¼ +issues.filter_type.all_pull_requests=ã™ã¹ã¦ã®ãƒ—ルリクエスト issues.filter_type.assigned_to_you=è‡ªåˆ†ãŒæ‹…当 issues.filter_type.created_by_you=自分ãŒä½œæˆ issues.filter_type.mentioning_you=自分ãŒé–¢ä¿‚ @@ -1651,6 +1655,7 @@ issues.save=ä¿å˜ issues.label_title=åå‰ issues.label_description=説明 issues.label_color=カラー +issues.label_color_invalid=無効ãªè‰²ã§ã™ issues.label_exclusive=排他 issues.label_archive=アーカイブ ラベル issues.label_archived_filter=アーカイブã•れãŸãƒ©ãƒ™ãƒ«ã‚’表示 @@ -2399,6 +2404,8 @@ settings.event_pull_request_review_request_desc=プルリクエストã®ãƒ¬ãƒ“ムsettings.event_pull_request_approvals=ãƒ—ãƒ«ãƒªã‚¯ã‚¨ã‚¹ãƒˆã®æ‰¿èª settings.event_pull_request_merge=プルリクエストã®ãƒžãƒ¼ã‚¸ settings.event_header_workflow=ワークフãƒãƒ¼ã‚¤ãƒ™ãƒ³ãƒˆ +settings.event_workflow_run=ワークフãƒãƒ¼å®Ÿè¡Œ +settings.event_workflow_run_desc=Gitea Actions ã®ãƒ¯ãƒ¼ã‚¯ãƒ•ãƒãƒ¼å®Ÿè¡ŒãŒã€ã‚ューã«è¿½åŠ ã€å¾…機ä¸ã€å®Ÿè¡Œä¸ã€å®Œäº†ã«ãªã£ãŸã¨ã。 settings.event_workflow_job=ワークフãƒãƒ¼ã‚¸ãƒ§ãƒ– settings.event_workflow_job_desc=Gitea Actions ã®ãƒ¯ãƒ¼ã‚¯ãƒ•ãƒãƒ¼ã‚¸ãƒ§ãƒ–ãŒã€ã‚ューã«è¿½åŠ ã€å¾…機ä¸ã€å®Ÿè¡Œä¸ã€å®Œäº†ã«ãªã£ãŸã¨ã。 settings.event_package=パッケージ @@ -2806,6 +2813,7 @@ team_permission_desc=æ¨©é™ team_unit_desc=リãƒã‚¸ãƒˆãƒªã®ã‚»ã‚¯ã‚·ãƒ§ãƒ³ã¸ã®ã‚¢ã‚¯ã‚»ã‚¹ã‚’è¨±å¯ team_unit_disabled=(無効) +form.name_been_taken=組織å "%s" ã¯æ—¢ã«ä½¿ç”¨ã•れã¦ã„ã¾ã™ã€‚ form.name_reserved=組織å "%s" ã¯äºˆç´„ã•れã¦ã„ã¾ã™ã€‚ form.name_pattern_not_allowed=`"%s" ã®å½¢å¼ã¯çµ„ç¹”åã«ä½¿ç”¨ã§ãã¾ã›ã‚“。` form.create_org_not_allowed=組織を作æˆã™ã‚‹æ¨©é™ãŒã‚りã¾ã›ã‚“。 @@ -2827,15 +2835,28 @@ settings.visibility.private_shortname=プライベート settings.update_settings=è¨å®šã®æ›´æ–° settings.update_setting_success=組織ã®è¨å®šã‚’æ›´æ–°ã—ã¾ã—ãŸã€‚ -settings.change_orgname_prompt=注æ„: 組織åを変更ã™ã‚‹ã¨çµ„ç¹”ã®URLも変更ã•れã€å¤ã„åå‰ã¯è§£æ”¾ã•れã¾ã™ã€‚ -settings.change_orgname_redirect_prompt=å¤ã„åå‰ã¯ã€å†ä½¿ç”¨ã•れã¦ã„ãªã„é™ã‚Šãƒªãƒ€ã‚¤ãƒ¬ã‚¯ãƒˆã—ã¾ã™ã€‚ + +settings.rename=組織åã®å¤‰æ›´ +settings.rename_desc=組織åを変更ã™ã‚‹ã¨çµ„ç¹”ã®URLも変更ã•れã€å¤ã„åå‰ã¯è§£æ”¾ã•れã¾ã™ã€‚ +settings.rename_success=組織 %[1]s ã® %[2]s ã¸ã®æ”¹åã«æˆåŠŸã—ã¾ã—ãŸã€‚ +settings.rename_no_change=組織åã®å¤‰æ›´ã¯ã‚りã¾ã›ã‚“。 +settings.rename_new_org_name=æ–°ã—ã„組織å +settings.rename_failed=内部エラーã®ãŸã‚組織åを変更ã§ãã¾ã›ã‚“ã§ã—㟠+settings.rename_notices_1=ã“ã®æ“作ã¯<strong>å…ƒã«æˆ»ã›ã¾ã›ã‚“</strong> 。 +settings.rename_notices_2=å¤ã„åå‰ã¯ã€å†ä½¿ç”¨ã•れるã¾ã§ã¯ãƒªãƒ€ã‚¤ãƒ¬ã‚¯ãƒˆã—ã¾ã™ã€‚ + settings.update_avatar_success=組織ã®ã‚¢ãƒã‚¿ãƒ¼ã‚’æ›´æ–°ã—ã¾ã—ãŸã€‚ settings.delete=組織を削除 settings.delete_account=ã“ã®çµ„織を削除 settings.delete_prompt=çµ„ç¹”ã¯æ’ä¹…çš„ã«å‰Šé™¤ã•れã¾ã™ã€‚ å…ƒã«æˆ»ã™ã“ã¨ã¯<strong>ã§ãã¾ã›ã‚“</strong>ï¼ +settings.name_confirm=確èªã®ãŸã‚組織åを入力: +settings.delete_notices_1=ã“ã®æ“作ã¯<strong>å…ƒã«æˆ»ã›ã¾ã›ã‚“</strong> 。 +settings.delete_notices_2=ã“ã®æ“作ã«ã‚ˆã‚Šã€<strong>%s</strong>ã®ã™ã¹ã¦ã®<strong>リãƒã‚¸ãƒˆãƒª</strong>ãŒæ’ä¹…çš„ã«å‰Šé™¤ã•れã¾ã™ã€‚ コードã€ã‚¤ã‚·ãƒ¥ãƒ¼ã€ã‚³ãƒ¡ãƒ³ãƒˆã€Wikiデータã€å…±åŒä½œæ¥è€…ã®è¨å®šã‚‚å«ã¾ã‚Œã¾ã™ã€‚ +settings.delete_notices_3=ã“ã®æ“作ã«ã‚ˆã‚Šã€<strong>%s</strong>ã®ã™ã¹ã¦ã®<strong>パッケージ</strong>ãŒæ’ä¹…çš„ã«å‰Šé™¤ã•れã¾ã™ã€‚ +settings.delete_notices_4=ã“ã®æ“作ã«ã‚ˆã‚Šã€<strong>%s</strong>ã®ã™ã¹ã¦ã®<strong>プãƒã‚¸ã‚§ã‚¯ãƒˆ</strong>ãŒæ’ä¹…çš„ã«å‰Šé™¤ã•れã¾ã™ã€‚ settings.confirm_delete_account=å‰Šé™¤ã‚’ç¢ºèª -settings.delete_org_title=組織ã®å‰Šé™¤ -settings.delete_org_desc=組織をæ’ä¹…çš„ã«å‰Šé™¤ã—ã¾ã™ã€‚ 続行ã—ã¾ã™ã‹ï¼Ÿ +settings.delete_failed=内部エラーã®ãŸã‚組織を削除ã§ãã¾ã›ã‚“ã§ã—㟠+settings.delete_successful=組織ã®<b>%s</b>ã®å‰Šé™¤ã«æˆåŠŸã—ã¾ã—ãŸã€‚ settings.hooks_desc=ã“ã®çµ„ç¹”ã®<strong>ã™ã¹ã¦ã®ãƒªãƒã‚¸ãƒˆãƒª</strong>ã§ãƒˆãƒªã‚¬ãƒ¼ã•れるWebhookã‚’è¿½åŠ ã—ã¾ã™ã€‚ settings.labels_desc=ã“ã®çµ„ç¹”ã®<strong>ã™ã¹ã¦ã®ãƒªãƒã‚¸ãƒˆãƒª</strong>ã§ä½¿ç”¨å¯èƒ½ãªã‚¤ã‚·ãƒ¥ãƒ¼ãƒ©ãƒ™ãƒ«ã‚’è¿½åŠ ã—ã¾ã™ã€‚ diff --git a/options/locale/locale_ko-KR.ini b/options/locale/locale_ko-KR.ini index 22bf3e1641..6f36ad905c 100644 --- a/options/locale/locale_ko-KR.ini +++ b/options/locale/locale_ko-KR.ini @@ -648,6 +648,7 @@ editor.filename_cannot_be_empty=파ì¼ëª…ì´ ë¹ˆì¹¸ìž…ë‹ˆë‹¤. editor.no_changes_to_show=í‘œì‹œí• ë³€ê²½ì‚¬í•ì´ ì—†ìŠµë‹ˆë‹¤. editor.add_subdir=경로 추가... + commits.desc=소스 코드 변경 ë‚´ì— íƒìƒ‰ commits.commits=커밋 commits.search_all=ëª¨ë“ ë¸Œëžœì¹˜ @@ -1151,12 +1152,12 @@ settings.visibility.private_shortname=비공개 settings.update_settings=ì„¤ì • ì—…ë°ì´íЏ settings.update_setting_success=ì¡°ì§ ì„¤ì •ì´ ë³€ê²½ë˜ì—ˆìŠµë‹ˆë‹¤. + + settings.update_avatar_success=ì¡°ì§ì˜ 아바타가 ê°±ì‹ ë˜ì—ˆìŠµë‹ˆë‹¤. settings.delete=ì¡°ì§ ì‚ì œ settings.delete_account=ì´ ì¡°ì§ì„ ì‚ì œí•©ë‹ˆë‹¤. settings.confirm_delete_account=ì‚ì œ ìŠ¹ì¸ -settings.delete_org_title=ì¡°ì§ ì‚ì œ -settings.delete_org_desc=ì´ ì¡°ì§ì´ ì˜êµ¬ížˆ ì‚ì œë©ë‹ˆë‹¤. ê³„ì† í•˜ì‹œê² ìŠµë‹ˆê¹Œ? members.membership_visibility=íšŒì› í‘œì‹œ: diff --git a/options/locale/locale_lv-LV.ini b/options/locale/locale_lv-LV.ini index a746f8738c..bb7f09e979 100644 --- a/options/locale/locale_lv-LV.ini +++ b/options/locale/locale_lv-LV.ini @@ -1196,7 +1196,6 @@ editor.update=Atjaunot %s editor.delete=DzÄ“st %s editor.patch=Pielietot ielÄpu editor.patching=Pielieto ielÄpu: -editor.fail_to_apply_patch=`NeizdevÄs pielietot ielÄpu "%s"` editor.new_patch=Jauns ielÄps editor.commit_message_desc=Pievienot neobligÄtu paplaÅ¡inÄtu aprakstu… editor.signoff_desc=Pievienot revÄ«zijas žurnÄla ziņojuma beigÄs Signed-off-by ar revÄ«zijas autoru. @@ -1214,15 +1213,11 @@ editor.branch_already_exists=Atzars "%s" Å¡ajÄ repozitorijÄ jau eksistÄ“. editor.directory_is_a_file=Direktorijas nosaukums "%s" vecÄka ceÄ¼Ä ir fails nevis direktorija Å¡ajÄ repozitorijÄ. editor.file_is_a_symlink=Fails "%s" ir norÄde, kuru nav iespÄ“jams labot no tÄ«mekļa redaktora editor.filename_is_a_directory=Faila nosaukums "%s" sakrÄ«t ar direktorijas nosaukumu Å¡ajÄ repozitorijÄ. -editor.file_editing_no_longer_exists=Fails "%s", ko labojat, vairs neeksistÄ“ Å¡ajÄ repozitorijÄ. -editor.file_deleting_no_longer_exists=Fails "%s", ko dzēšat, vairs neeksistÄ“ Å¡ajÄ repozitorijÄ. editor.file_changed_while_editing=Faila saturs ir mainÄ«jies kopÅ¡ sÄkÄt to labot. Noklikšķiniet <a target="_blank" rel="noopener noreferrer" href="%s">Å¡eit</a>, lai apskatÄ«tu, vai <strong>NosÅ«tiet izmaiņas atkÄrtoti</strong>, lai pÄrrakstÄ«tu. editor.file_already_exists=Fails ar nosaukumu "%s" Å¡ajÄ repozitorijÄ jau eksistÄ“. editor.commit_empty_file_header=IesÅ«tÄ«t tukÅ¡u failu editor.commit_empty_file_text=Fails, ko vÄ“laties iesÅ«tÄ«t, ir tukÅ¡s. Vai turpinÄt? editor.no_changes_to_show=Nav izmaiņu, ko rÄdÄ«t. -editor.fail_to_update_file=NeizdevÄs atjaunot/izveidot failu "%s". -editor.fail_to_update_file_summary=Kļūdas ziņojums: editor.push_rejected_no_message=Izmaiņu iesÅ«tīšana tika noraidÄ«ta, bet serveris neatgrieza paziņojumu. PÄrbaudiet git ÄÄ·us Å¡im repozitorijam. editor.push_rejected=Serveris noraidÄ«ja Å¡o izmaiņu. PÄrbaudiet git ÄÄ·us. editor.push_rejected_summary=Pilns noraidīšanas ziņojums: @@ -1237,6 +1232,7 @@ editor.require_signed_commit=AtzarÄ var iesÅ«tÄ«t tikai parakstÄ«tas revÄ«zijas editor.cherry_pick=IzlasÄ«t %s uz: editor.revert=Atgriezt %s uz: + commits.desc=PÄrlÅ«kot pirmkoda izmaiņu vÄ“sturi. commits.commits=RevÄ«zijas commits.no_commits=Nav kopÄ«gu revÄ«ziju. Atzariem "%s" un "%s" ir pilnÄ«bÄ atšķirÄ«ga izmaiņu vÄ“sture. @@ -2509,15 +2505,13 @@ settings.visibility.private_shortname=PrivÄta settings.update_settings=MainÄ«t iestatÄ«jumus settings.update_setting_success=OrganizÄcijas iestatÄ«jumi tika saglabÄti. -settings.change_orgname_prompt=PiezÄ«me: organizÄcijas nosaukuma maiņa izmainÄ«s arÄ« organizÄcijas URL un atbrÄ«vos veco nosaukumu. -settings.change_orgname_redirect_prompt=Vecais vÄrds pÄrsÅ«tÄ«s uz jauno, kamÄ“r vien tas nebÅ«s izmantots. + + settings.update_avatar_success=OrganizÄcijas attÄ“ls tika saglabÄts. settings.delete=DzÄ“st organizÄciju settings.delete_account=DzÄ“st Å¡o organizÄciju settings.delete_prompt=Å Ä« darbÄ«ba pilnÄ«bÄ dzÄ“sÄ«s Å¡o organizÄciju, kÄ arÄ« tÄ ir <strong>NEATGRIEZENISKA</strong>! settings.confirm_delete_account=ApstiprinÄt dzēšanu -settings.delete_org_title=DzÄ“st organizÄciju -settings.delete_org_desc=OrganizÄcija tiks dzÄ“sta neatgriezeniski. Vai turpinÄt? settings.labels_desc=Pievienojiet iezÄ«mes, kas var tikt izmantotas <strong>visos</strong> šīs organizÄcijas repozitorijos. diff --git a/options/locale/locale_nl-NL.ini b/options/locale/locale_nl-NL.ini index b6887ee9e0..693e8cbb60 100644 --- a/options/locale/locale_nl-NL.ini +++ b/options/locale/locale_nl-NL.ini @@ -1012,7 +1012,6 @@ editor.file_changed_while_editing=De bestandsinhoud is veranderd sinds je bent b editor.commit_empty_file_header=Commit een leeg bestand editor.commit_empty_file_text=Het bestand dat u wilt committen is leeg. Doorgaan? editor.no_changes_to_show=Er zijn geen wijzigingen om weer te geven. -editor.fail_to_update_file_summary=Foutmelding: editor.push_rejected_no_message=De wijziging is afgewezen door de server zonder bericht. Controleer de Git Hooks alsjeblieft. editor.push_rejected=De wijziging is afgewezen door de server. Controleer Controleer de Git Hooks alsjeblieft. editor.push_rejected_summary=Volledig afwijzingsbericht: @@ -1023,6 +1022,7 @@ editor.require_signed_commit=Branch vereist een ondertekende commit editor.cherry_pick=Cherry-pick %s op: editor.revert=%s ongedaan maken op: + commits.desc=Bekijk de broncode-wijzigingsgeschiedenis. commits.commits=Commits commits.nothing_to_compare=Deze branches zijn gelijk. @@ -1989,13 +1989,13 @@ settings.visibility.private=Privé (alleen zichtbaar voor organisatieleden) settings.visibility.private_shortname=Privé settings.update_settings=Instellingen bijwerken + + settings.update_avatar_success=De avatar van de organisatie is aangepast. settings.delete=Verwijder organisatie settings.delete_account=Verwijder deze organisatie settings.delete_prompt=Deze organisatie zal permanent worden verwijderd. U kunt dit <strong>NIET</strong> ongedaan maken! settings.confirm_delete_account=Bevestig verwijdering -settings.delete_org_title=Verwijder organisatie -settings.delete_org_desc=Deze organisatie zal permanent verwijderd worden. Doorgaan? settings.hooks_desc=Een webhook toevoegen die door <strong>alle repositories</strong> in deze organisatie getriggerd kan worden. settings.labels_desc=Voeg labels toe die kunnen worden gebruikt bij problemen voor <strong>alle repositories</strong> in deze organisatie. diff --git a/options/locale/locale_pl-PL.ini b/options/locale/locale_pl-PL.ini index 42a33f9ce4..6f8f5394ae 100644 --- a/options/locale/locale_pl-PL.ini +++ b/options/locale/locale_pl-PL.ini @@ -942,13 +942,13 @@ editor.file_changed_while_editing=Zawartość pliku zmieniÅ‚a siÄ™, odkÄ…d rozpo editor.commit_empty_file_header=Commituj pusty plik editor.commit_empty_file_text=Plik, który zamierzasz commitować, jest pusty. Kontynuować? editor.no_changes_to_show=Brak zmian do pokazania. -editor.fail_to_update_file_summary=Komunikat błędu: editor.push_rejected_summary=PeÅ‚ny komunikat odrzucenia: editor.add_subdir=Dodaj katalog… editor.no_commit_to_branch=Zatwierdzanie bezpoÅ›rednio do tej gałęzi nie jest możliwe, ponieważ: editor.user_no_push_to_branch=Użytkownik nie może wypychać do gałęzi editor.require_signed_commit=Gałąź wymaga podpisanych commitów + commits.desc=PrzeglÄ…daj historiÄ™ zmian kodu źródÅ‚owego. commits.commits=Commity commits.search_all=Wszystkie gałęzie @@ -1862,14 +1862,13 @@ settings.visibility.private_shortname=Prywatny settings.update_settings=Aktualizuj ustawienia settings.update_setting_success=Ustawienia organizacji zostaÅ‚y zaktualizowane. -settings.change_orgname_redirect_prompt=Stara nazwa bÄ™dzie przekierowywaÅ‚a dopóki ktoÅ› jej nie zajmie. + + settings.update_avatar_success=Awatar organizacji zostaÅ‚ zaktualizowany. settings.delete=UsuÅ„ organizacjÄ™ settings.delete_account=UsuÅ„ tÄ… organizacjÄ™ settings.delete_prompt=Organizacja zostanie trwale usuniÄ™ta. Tej akcji <strong>NIE MOÅ»NA</strong> cofnąć! settings.confirm_delete_account=Potwierdź usuniÄ™cie -settings.delete_org_title=UsuÅ„ organizacjÄ™ -settings.delete_org_desc=Ta organizacja zostanie trwale usuniÄ™ta. Kontynuować? settings.hooks_desc=Dodaj webhooki, uruchamiane dla <strong>wszystkich repozytoriów</strong> w tej organizacji. settings.labels_desc=Dodaj etykiety, które mogÄ… być używane w zgÅ‚oszeniach dla <strong>wszystkich repozytoriów</strong> w tej organizacji. diff --git a/options/locale/locale_pt-BR.ini b/options/locale/locale_pt-BR.ini index 8ee675e6e0..c67b2bb408 100644 --- a/options/locale/locale_pt-BR.ini +++ b/options/locale/locale_pt-BR.ini @@ -1191,7 +1191,6 @@ editor.update=Atualizar %s editor.delete=Excluir %s editor.patch=Aplicar Correção editor.patching=Corrigindo: -editor.fail_to_apply_patch=`Não foi possÃvel aplicar a correção "%s"` editor.new_patch=Nova correção editor.commit_message_desc=Adicione uma descrição detalhada (opcional)... editor.signoff_desc=Adicione um assinado-por-committer no final do log do commit. @@ -1209,15 +1208,11 @@ editor.branch_already_exists=Branch "%s" já existe neste repositório. editor.directory_is_a_file=O nome do diretório "%s" já é usado como um nome de arquivo neste repositório. editor.file_is_a_symlink=`"%s" é um link simbólico. Links simbólicos não podem ser editados no editor da web` editor.filename_is_a_directory=O nome do arquivo "%s" já é usado como um nome de diretório neste repositório. -editor.file_editing_no_longer_exists=O arquivo que está sendo editado, "%s", não existe mais neste repositório. -editor.file_deleting_no_longer_exists=O arquivo a ser excluÃdo, "%s", não existe mais neste repositório. editor.file_changed_while_editing=O conteúdo do arquivo mudou desde que você começou a editar. <a target="_blank" rel="noopener noreferrer" href="%s">Clique aqui</a> para ver o que foi editado ou <strong>clique em Aplicar commit das alterações novamemente</strong> para sobreescrever estas alterações. editor.file_already_exists=Um arquivo com nome "%s" já existe neste repositório. editor.commit_empty_file_header=Fazer commit de um arquivo vazio editor.commit_empty_file_text=O arquivo que você está prestes fazer commit está vazio. Continuar? editor.no_changes_to_show=Nenhuma alteração a mostrar. -editor.fail_to_update_file=Falha ao atualizar/criar arquivo "%s". -editor.fail_to_update_file_summary=Mensagem de erro: editor.push_rejected_no_message=A alteração foi rejeitada pelo servidor sem uma mensagem. Por favor, verifique os Hooks Git. editor.push_rejected=A alteração foi rejeitada pelo servidor. Por favor, verifique os Hooks Git. editor.push_rejected_summary=Mensagem completa de rejeição: @@ -1232,6 +1227,7 @@ editor.require_signed_commit=Branch requer um commit assinado editor.cherry_pick=Cherry-pick %s para: editor.revert=Reverter %s para: + commits.desc=Veja o histórico de alterações do código de fonte. commits.commits=Commits commits.no_commits=Nenhum commit em comum. "%s" e "%s" tem históricos completamente diferentes. @@ -2468,14 +2464,13 @@ settings.visibility.private_shortname=Privado settings.update_settings=Atualizar Configurações settings.update_setting_success=Configurações da organização foram atualizadas. -settings.change_orgname_redirect_prompt=O nome antigo irá redirecionar até que seja reivindicado. + + settings.update_avatar_success=O avatar da organização foi atualizado. settings.delete=Excluir organização settings.delete_account=Excluir esta organização settings.delete_prompt=A organização será excluÃda permanentemente. Isto <strong>NÃO PODERÃ</strong> ser desfeito! settings.confirm_delete_account=Confirmar exclusão -settings.delete_org_title=Excluir organização -settings.delete_org_desc=Essa organização será excluÃda permanentemente. Continuar? settings.hooks_desc=Adicionar Webhooks que serão acionados para <strong>todos os repositórios</strong> desta organização. settings.labels_desc=Adicionar rótulos que possam ser usadas em issues para <strong>todos os repositórios</strong> desta organização. diff --git a/options/locale/locale_pt-PT.ini b/options/locale/locale_pt-PT.ini index a62a24ff1d..cb40a5a784 100644 --- a/options/locale/locale_pt-PT.ini +++ b/options/locale/locale_pt-PT.ini @@ -1353,7 +1353,6 @@ editor.update=Modificar %s editor.delete=Eliminar %s editor.patch=Aplicar remendo (patch) editor.patching=Remendando (patching): -editor.fail_to_apply_patch=`Não foi possÃvel aplicar o remendo (patch) "%s"` editor.new_patch=Novo remendo (patch) editor.commit_message_desc=Adicionar uma descrição alargada opcional… editor.signoff_desc=Adicionar "Assinado-por" seguido do autor do cometimento no fim da mensagem do registo de cometimentos. @@ -1373,8 +1372,6 @@ editor.branch_already_exists=O ramo "%s" já existe neste repositório. editor.directory_is_a_file=O nome da pasta "%s" já é usado como um nome de ficheiro neste repositório. editor.file_is_a_symlink=`"%s" é uma ligação simbólica. Ligações simbólicas não podem ser editadas no editor web` editor.filename_is_a_directory=O nome de ficheiro "%s" já está a ser usado como um nome de pasta neste repositório. -editor.file_editing_no_longer_exists=O ficheiro que está a ser editado, "%s", já não existe neste repositório. -editor.file_deleting_no_longer_exists=O ficheiro que está a ser eliminado, "%s", já não existe neste repositório. editor.file_changed_while_editing=O conteúdo do ficheiro mudou desde que começou a editar. <a target="_blank" rel="noopener noreferrer" href="%s">Clique aqui</a> para ver as modificações ou clique em <strong>Cometer novamente</strong> para escrever por cima. editor.file_already_exists=Já existe um ficheiro com o nome "%s" neste repositório. editor.commit_id_not_matching=O ID do cometimento não corresponde ao ID de quando começou a editar. Faça o cometimento para um ramo de remendo (patch) e depois faça a integração. @@ -1382,8 +1379,6 @@ editor.push_out_of_date=O envio parece estar obsoleto. editor.commit_empty_file_header=Cometer um ficheiro vazio editor.commit_empty_file_text=O ficheiro que está prestes a cometer está vazio. Quer continuar? editor.no_changes_to_show=Não existem modificações para mostrar. -editor.fail_to_update_file=Falhou ao modificar/criar o ficheiro "%s". -editor.fail_to_update_file_summary=Mensagem de erro: editor.push_rejected_no_message=A modificação foi rejeitada pelo servidor sem qualquer mensagem. Verifique os Automatismos do Git. editor.push_rejected=A modificação foi rejeitada pelo servidor. Verifique os Automatismos do Git. editor.push_rejected_summary=Mensagem completa de rejeição: @@ -1398,6 +1393,7 @@ editor.require_signed_commit=O ramo requer um cometimento assinado editor.cherry_pick=Escolher a dedo %s para: editor.revert=Reverter %s para: + commits.desc=Navegar pelo histórico de modificações no código fonte. commits.commits=Cometimentos commits.no_commits=Não há cometimentos em comum. "%s" e "%s" têm históricos completamente diferentes. @@ -2402,8 +2398,10 @@ settings.event_pull_request_review_request_desc=A revisão do pedido de integraà settings.event_pull_request_approvals=Aprovações do pedido de integração settings.event_pull_request_merge=Integração constante no pedido settings.event_header_workflow=Eventos da sequência de trabalho +settings.event_workflow_run=Execução da sequência de trabalho +settings.event_workflow_run_desc=A execução da sequência de trabalho das operações do Gitea foi colocada em fila, está em espera, em andamento ou concluÃda. settings.event_workflow_job=Trabalhos da sequência de trabalho -settings.event_workflow_job_desc=O trabalho da sequência de trabalho das operações do Gitea foi colocado em fila, está em espera, em andamento ou concluÃda. +settings.event_workflow_job_desc=O trabalho da sequência de trabalho das operações do Gitea foi colocado em fila, está em espera, em andamento ou concluÃdo. settings.event_package=Pacote settings.event_package_desc=Pacote criado ou eliminado num repositório. settings.branch_filter=Filtro de ramos @@ -2830,15 +2828,13 @@ settings.visibility.private_shortname=Privado settings.update_settings=Modificar configurações settings.update_setting_success=As configurações da organização foram modificadas. -settings.change_orgname_prompt=Nota: Mudar o nome da organização também irá mudar o URL da organização e libertar o nome antigo. -settings.change_orgname_redirect_prompt=O nome antigo, enquanto não for reivindicado, irá reencaminhar para o novo. + + settings.update_avatar_success=O avatar da organização foi modificado. settings.delete=Eliminar organização settings.delete_account=Eliminar esta organização settings.delete_prompt=A organização será removida permanentemente. Essa operação <strong>NÃO PODERÃ</strong> ser revertida! settings.confirm_delete_account=Confirme a eliminação -settings.delete_org_title=Eliminar organização -settings.delete_org_desc=Esta organização será eliminada permanentemente. Quer continuar? settings.hooks_desc=Adicionar automatismos web que serão despoletados para <strong>todos os repositórios</strong> desta organização. settings.labels_desc=Adicionar rótulos que possam ser usados em questões para <strong>todos os repositórios</strong> desta organização. diff --git a/options/locale/locale_ru-RU.ini b/options/locale/locale_ru-RU.ini index c65d08a4cf..c5f8e53eff 100644 --- a/options/locale/locale_ru-RU.ini +++ b/options/locale/locale_ru-RU.ini @@ -1169,7 +1169,6 @@ editor.update=Обновить %s editor.delete=Удалить %s editor.patch=Применить патч editor.patching=ИÑправление: -editor.fail_to_apply_patch=Ðевозможно применить патч «%s» editor.new_patch=Ðовый патч editor.commit_message_desc=Добавьте необÑзательное раÑширенное опиÑание… editor.signoff_desc=Добавить трейлер Signed-off-by Ñ Ð°Ð²Ñ‚Ð¾Ñ€Ð¾Ð¼ коммита в конце ÑÐ¾Ð¾Ð±Ñ‰ÐµÐ½Ð¸Ñ ÐºÐ¾Ð¼Ð¼Ð¸Ñ‚Ð°. @@ -1187,15 +1186,11 @@ editor.branch_already_exists=Ветка «%s» уже ÑущеÑтвует в Ñ editor.directory_is_a_file=Ð˜Ð¼Ñ ÐºÐ°Ñ‚Ð°Ð»Ð¾Ð³Ð° «%s» уже иÑпользуетÑÑ Ð² качеÑтве имени файла в Ñтом репозитории. editor.file_is_a_symlink=`«%s» ÑвлÑетÑÑ ÑимволичеÑкой ÑÑылкой. СимволичеÑкие ÑÑылки невозможно отредактировать в веб-редакторе` editor.filename_is_a_directory=Ð˜Ð¼Ñ Ñ„Ð°Ð¹Ð»Ð° «%s» уже иÑпользуетÑÑ Ð² качеÑтве каталога в Ñтом репозитории. -editor.file_editing_no_longer_exists=Редактируемый файл «%s» больше не ÑущеÑтвует в Ñтом репозитории. -editor.file_deleting_no_longer_exists=УдалÑемый файл «%s» больше не ÑущеÑтвует в Ñтом репозитории. editor.file_changed_while_editing=Содержимое файла изменилоÑÑŒ Ñ Ð¼Ð¾Ð¼ÐµÐ½Ñ‚Ð° начала редактированиÑ. <a target="_blank" rel="noopener noreferrer" href="%s">Ðажмите здеÑÑŒ</a>, чтобы увидеть, что было изменено, или <strong>ЗафикÑировать Ð¸Ð·Ð¼ÐµÐ½ÐµÐ½Ð¸Ñ Ñнова</strong>, чтобы заменить их. editor.file_already_exists=Файл Ñ Ð¸Ð¼ÐµÐ½ÐµÐ¼ «%s» уже ÑущеÑтвует в репозитории. editor.commit_empty_file_header=Закоммитить пуÑтой файл editor.commit_empty_file_text=Файл, который вы ÑобираетеÑÑŒ зафикÑировать, пуÑÑ‚. Продолжить? editor.no_changes_to_show=Ðет изменений. -editor.fail_to_update_file=Ðе удалоÑÑŒ обновить/Ñоздать файл «%s». -editor.fail_to_update_file_summary=Ошибка: editor.push_rejected_no_message=Изменение отклонено Ñервером без ÑообщениÑ. ПожалуйÑта, проверьте хуки Git. editor.push_rejected=Изменение отклонено Ñервером. ПожалуйÑта, проверьте хуки Git. editor.push_rejected_summary=Полное Ñообщение об отклонении: @@ -1210,6 +1205,7 @@ editor.require_signed_commit=Ветка ожидает подпиÑанный к editor.cherry_pick=ПеренеÑти Ð¸Ð·Ð¼ÐµÐ½ÐµÐ½Ð¸Ñ %s в: editor.revert=Откатить %s к: + commits.desc=ПроÑмотр иÑтории изменений иÑходного кода. commits.commits=Коммитов commits.no_commits=Ðет общих коммитов. «%s» и «%s» имеют Ñовершенно разные иÑтории. @@ -2455,15 +2451,13 @@ settings.visibility.private_shortname=Приватный settings.update_settings=Обновить наÑтройки settings.update_setting_success=ÐаÑтройки организации обновлены. -settings.change_orgname_prompt=Обратите внимание: изменение Ð½Ð°Ð·Ð²Ð°Ð½Ð¸Ñ Ð¾Ñ€Ð³Ð°Ð½Ð¸Ð·Ð°Ñ†Ð¸Ð¸ также изменит URL вашей организации и оÑвободит Ñтарое имÑ. -settings.change_orgname_redirect_prompt=Старое Ð¸Ð¼Ñ Ð±ÑƒÐ´ÐµÑ‚ перенаправлено до тех пор, пока оно не будет введено. + + settings.update_avatar_success=Ðватар организации обновлён. settings.delete=Удалить организацию settings.delete_account=Удалить Ñту организацию settings.delete_prompt=Ðто дейÑтвие <strong>БЕЗВОЗВРÐТÐО</strong> удалит Ñту организацию навÑегда! settings.confirm_delete_account=Подтвердить удаление -settings.delete_org_title=Удалить организацию -settings.delete_org_desc=Ðта Ð¾Ñ€Ð³Ð°Ð½Ð¸Ð·Ð°Ñ†Ð¸Ñ Ð±ÑƒÐ´ÐµÑ‚ безвозвратно удалена. Продолжить? settings.hooks_desc=Добавьте веб-хуки, которые будет вызыватьÑÑ Ð´Ð»Ñ <strong>вÑех репозиториев</strong> под Ñтой организации. settings.labels_desc=Добавьте метки, которые могут быть иÑпользованы в задачах Ð´Ð»Ñ <strong>вÑех репозиториев</strong> Ñтой организации. diff --git a/options/locale/locale_si-LK.ini b/options/locale/locale_si-LK.ini index a209187aff..c1aeff87c2 100644 --- a/options/locale/locale_si-LK.ini +++ b/options/locale/locale_si-LK.ini @@ -917,13 +917,13 @@ editor.file_changed_while_editing=ඔබ සංස්කරණය කිරීචeditor.commit_empty_file_header=හිස් ගොනුවක් à¶šà·à¶´ කරන්න editor.commit_empty_file_text=ඔබ à¶šà·à¶´ කිරීමට යන ගොනුව හිස් ය. ඉදිරියට? editor.no_changes_to_show=පෙන්වීමට කිසිදු වෙනසක් à¶±à·à¶. -editor.fail_to_update_file_summary=දà·à·‚ පණිවිඩය: editor.push_rejected_summary=පූර්ණ à¶´à·Šà¶»à¶à·’à¶šà·Šà·‚à·šà¶´ පණිවිඩය: editor.add_subdir=ඩිරෙක්ටරියක් à¶‘à¶šà·Š කරන්න… editor.no_commit_to_branch=à·à·à¶›à·à·€à¶§ කෙලින්ම à¶šà·à¶´à·€à·’ය නොහà·à¶šà·’ නිසà·: editor.user_no_push_to_branch=පරිà·à·“ලකයà·à¶§ à·à·à¶›à·à·€à¶§ à¶à¶½à·Šà¶½à·” à¶šà·… නොහà·à¶š editor.require_signed_commit=à·à·à¶›à·à·€à¶§ à¶…à¶à·Šà·ƒà¶±à·Š à¶šà·… à¶šà·à¶´à·€à·“මක් à¶…à·€à·à·Šà¶º වේ + commits.desc=මූලà·à·à·Šà¶» කේචවෙනස් කිරීමේ ඉà¶à·’à·„à·à·ƒà¶º පිරික්සන්න. commits.commits=විවරයන් commits.nothing_to_compare=මෙම à·à·à¶›à· සමà·à¶± වේ. @@ -1882,14 +1882,13 @@ settings.visibility.private_shortname=පෞද්ගලික settings.update_settings=à·ƒà·à¶šà·ƒà·”ම් යà·à·€à¶à·Šà¶šà·à¶½ කරන්න settings.update_setting_success=සංවිධà·à¶±à¶ºà·š à·ƒà·à¶šà·ƒà·”ම් යà·à·€à¶à·Šà¶šà·à¶½ à¶šà¶» ඇà¶. -settings.change_orgname_redirect_prompt=à¶´à·à¶»à¶«à·’ නම ඉල්ල෠සිටින à¶à·”රු à¶±à·à·€à¶ හරව෠යවයි. + + settings.update_avatar_success=සංවිධà·à¶±à¶ºà·š à¶…à·€à¶à·à¶»à¶º යà·à·€à¶à·Šà¶šà·à¶½à·“à¶± à¶šà¶» ඇà¶. settings.delete=සංවිධà·à¶±à¶º මකන්න settings.delete_account=මෙම සංවිධà·à¶±à¶º මකන්න settings.delete_prompt=සංවිධà·à¶±à¶º ස්ථිරවම ඉවà¶à·Š කරනු à¶½à·à¶¶à·š. මෙම <strong></strong> à¶…à·„à·à·ƒà·’ à¶šà·… නොහà·à¶š! settings.confirm_delete_account=මකà·à¶¯à·à¶¸à·“ම à¶à·„වුරු කරන්න -settings.delete_org_title=සංවිධà·à¶±à¶º මකන්න -settings.delete_org_desc=මෙම සංවිධà·à¶±à¶º ස්ථිරවම මක෠දමනු ඇà¶. දිගටම? settings.hooks_desc=මෙම සංවිධà·à¶±à¶º යටà¶à·š <strong>සියලුම ගබඩà·à·€à¶±à·Š</strong> සඳහ෠මුලපුරනු ලබන වෙබ් කොකු à¶‘à¶šà¶à·” කරන්න. settings.labels_desc=මෙම සංවිධà·à¶±à¶º යටà¶à·š <strong>සියලුම ගබඩà·à·€à¶½à¶¯à·“</strong> සඳහ෠ගà·à¶§à·…à·” සඳහ෠භà·à·€à·’à¶à· à¶šà·… à·„à·à¶šà·’ ලේබල් à¶‘à¶šà¶à·” කරන්න. diff --git a/options/locale/locale_sk-SK.ini b/options/locale/locale_sk-SK.ini index e461075e53..5ea6a7508e 100644 --- a/options/locale/locale_sk-SK.ini +++ b/options/locale/locale_sk-SK.ini @@ -1007,6 +1007,7 @@ editor.commit_empty_file_text=Súbor, ktorý sa chystáte odoslaÅ¥, je prázdny. editor.no_commit_to_branch=Nedá sa odoslaÅ¥ priamo do vetvy, pretože: editor.require_signed_commit=Vetva vyžaduje podpÃsaný commit + commits.commits=Commity commits.search_all=VÅ¡etky vetvy commits.author=Autor @@ -1219,6 +1220,8 @@ lower_repositories=repozitáre settings.visibility.private=Súkromná ​​(viditeľné iba pre Älenov organizácie) settings.visibility.private_shortname=Súkromný + + settings.hooks_desc=Pridajte webhooky, ktoré sa spustia nad <strong>vÅ¡etkými repozitármi</strong> v rámci tejto organizácie. diff --git a/options/locale/locale_sv-SE.ini b/options/locale/locale_sv-SE.ini index 04428aeab2..7041db9ac3 100644 --- a/options/locale/locale_sv-SE.ini +++ b/options/locale/locale_sv-SE.ini @@ -777,12 +777,12 @@ editor.file_changed_while_editing=Filens innehÃ¥ll har ändrats sedan du pÃ¥bör editor.commit_empty_file_header=Committa en tom fil editor.commit_empty_file_text=Filen du vill committa är tom. Vill du fortsätta? editor.no_changes_to_show=Det finns inga ändringar att visa. -editor.fail_to_update_file_summary=Felmeddelande: editor.add_subdir=Lägga till en katalog… editor.no_commit_to_branch=Det gick inte att committa direkt till branchen för: editor.user_no_push_to_branch=Användaren kan inte pusha till branchen editor.require_signed_commit=Branchen kräver en signerad commit + commits.desc=Bläddra i källkodens förändringshistorik. commits.commits=Incheckningar commits.search_all=Alla brancher @@ -1524,13 +1524,13 @@ settings.visibility.private=Privat (synlig endast för organisationens medlemmar settings.visibility.private_shortname=Privat settings.update_settings=Uppdatera inställningar + + settings.update_avatar_success=Organisationens avatar har uppdateras. settings.delete=Tag bort organisation settings.delete_account=Tag bort denna organisation settings.delete_prompt=Organisationen kommer tas bort permanent, och det gÃ¥r <strong>INTE</strong> att Ã¥ngra detta! settings.confirm_delete_account=Bekräfta borttagning -settings.delete_org_title=Ta bort organisation -settings.delete_org_desc=Denna organisation kommer tas bort permanent. Vill du fortsätta? settings.hooks_desc=Lägg till webbhook som triggas för <strong>alla utvecklingskataloger</strong> under denna organisationen. settings.labels_desc=Lägg till etiketter som kan användas till ärenden för <strong>alla utvecklingskataloger</strong> under denna organisation. diff --git a/options/locale/locale_tr-TR.ini b/options/locale/locale_tr-TR.ini index 1f46369fe0..f873c38023 100644 --- a/options/locale/locale_tr-TR.ini +++ b/options/locale/locale_tr-TR.ini @@ -1345,7 +1345,6 @@ editor.update=%s Güncelle editor.delete=%s Sil editor.patch=Yama Uygula editor.patching=Yamalanıyor: -editor.fail_to_apply_patch=`"%s" yaması uygulanamıyor` editor.new_patch=Yeni Yama editor.commit_message_desc=İsteÄŸe baÄŸlı uzun bir açıklama ekleyin… editor.signoff_desc=İşleme günlüğü mesajının sonuna iÅŸleyen tarafından imzalanan bir fragman ekleyin. @@ -1365,8 +1364,6 @@ editor.branch_already_exists=Bu depoda "%s" dalı zaten var. editor.directory_is_a_file=Dizin adı "%s" zaten bu depoda bir dosya adı olarak kullanılmaktadır. editor.file_is_a_symlink=`"%s" sembolik bir baÄŸlantıdır. Sembolik baÄŸlantılar web düzenleyicisinde düzenlenemez` editor.filename_is_a_directory=Dosya adı "%s" zaten bu depoda bir dizin adı olarak kullanılmaktadır. -editor.file_editing_no_longer_exists=Düzenlenmekte olan "%s" dosyası artık bu depoda yer almıyor. -editor.file_deleting_no_longer_exists=Silinen "%s" dosyası artık bu depoda yer almıyor. editor.file_changed_while_editing=Düzenlemeye baÅŸladığınızdan beri dosya içeriÄŸi deÄŸiÅŸti. Görmek için <a target="_blank" rel="noopener noreferrer" href="%s">burayı tıklayın</a> veya üzerine yazmak için <strong>deÄŸiÅŸiklikleri yine de iÅŸleyin</strong>. editor.file_already_exists=Bu depoda "%s" isimli bir dosya zaten var. editor.commit_id_not_matching=İşleme ID'si, düzenlemeye baÅŸladığınız ID ile uyuÅŸmuyor, bir yama dalına iÅŸleme yapın ve sonra birleÅŸtirin. @@ -1374,8 +1371,6 @@ editor.push_out_of_date=İtme eskimiÅŸ. editor.commit_empty_file_header=BoÅŸ bir dosya iÅŸle editor.commit_empty_file_text=İşlemek üzere olduÄŸunuz dosya boÅŸ. Devam edilsin mi? editor.no_changes_to_show=Gösterilecek deÄŸiÅŸiklik yok. -editor.fail_to_update_file=`"%s" dosyası güncellenemedi/oluÅŸturulamadı.` -editor.fail_to_update_file_summary=Hata Mesajı: editor.push_rejected_no_message=DeÄŸiÅŸiklik, bir ileti olmadan sunucu tarafından reddedildi. Git Hooks'u kontrol edin. editor.push_rejected=DeÄŸiÅŸiklik sunucu tarafından reddedildi. Lütfen Git Hooks'u kontrol edin. editor.push_rejected_summary=Tam Red Mesajı: @@ -1390,6 +1385,7 @@ editor.require_signed_commit=Dal imzalı bir iÅŸleme gerektirir editor.cherry_pick=%s ÅŸunun üzerine cımbızla: editor.revert=%s ÅŸuna geri döndür: + commits.desc=Kaynak kodu deÄŸiÅŸiklik geçmiÅŸine göz atın. commits.commits=İşleme commits.no_commits=Ortak bir iÅŸleme yok. "%s" ve "%s" tamamen farklı geçmiÅŸlere sahip. @@ -2735,15 +2731,13 @@ settings.visibility.private_shortname=Özel settings.update_settings=Ayarları Güncelle settings.update_setting_success=Organizasyon ayarları güncellendi. -settings.change_orgname_prompt=Not: Organizasyon adını deÄŸiÅŸtirmek organizasyonunuzun URL'sini de deÄŸiÅŸtirecek ve eski ismi serbest bıracaktır. -settings.change_orgname_redirect_prompt=Eski ad, talep edilene kadar yeniden yönlendirilecektir. + + settings.update_avatar_success=Organizasyonun resmi güncellendi. settings.delete=Organizasyonu Sil settings.delete_account=Bu Organizasyonu Sil settings.delete_prompt=Organizasyon kalıcı olarak kaldırılacaktır. Bu iÅŸlem <strong>GERİ ALINAMAZ</strong>! settings.confirm_delete_account=Silmeyi Onaylıyorum -settings.delete_org_title=Organizasyonu Sil -settings.delete_org_desc=Bu organizasyon kalıcı olarak silinecektir. Devam edilsin mi? settings.hooks_desc=Bu organizasyon altındaki <strong>tüm depolar</strong> için tetiklenecek webhook'lar ekle. settings.labels_desc=Bu organizasyonun altındaki <strong>tüm depolar</strong> ile ilgili konularda kullanılabilecek etiketler ekleyin. diff --git a/options/locale/locale_uk-UA.ini b/options/locale/locale_uk-UA.ini index ac6a089e13..bace83e76b 100644 --- a/options/locale/locale_uk-UA.ini +++ b/options/locale/locale_uk-UA.ini @@ -92,6 +92,7 @@ remove=Видалити remove_all=Видалити вÑе remove_label_str=`Видалити елемент "%s"` edit=Редагувати +view=ПереглÑнути test=ТеÑÑ‚ enabled=Увімкнено @@ -414,6 +415,7 @@ remember_me.compromised=Токен Ð´Ð»Ñ Ð²Ñ…Ð¾Ð´Ñƒ більше не дійÑÐ forgot_password_title=Забув пароль forgot_password=Забули пароль? need_account=Потрібен обліковий запиÑ? +sign_up_tip=Ви реєÑтруєте перший обліковий Ð·Ð°Ð¿Ð¸Ñ Ñƒ ÑиÑтемі, з правами адмініÑтратора. Будь лаÑка, уважно запам'Ñтайте Ñвоє ім'Ñ ÐºÐ¾Ñ€Ð¸Ñтувача та пароль. Якщо ви Ñ—Ñ… забудете, звернітьÑÑ Ð´Ð¾ документації Gitea, щоб відновити обліковий запиÑ. sign_up_now=ЗареєÑтруватиÑÑ. sign_up_successful=Обліковий Ð·Ð°Ð¿Ð¸Ñ Ñтворено уÑпішно. Вітаю! confirmation_mail_sent_prompt_ex=Ðовий лиÑÑ‚ з підтвердженнÑм було надіÑлано на <b>%s</b>. Будь лаÑка, перевірте Ñвою поштову Ñкриньку протÑгом наÑтупних %s, щоб завершити Ð¿Ñ€Ð¾Ñ†ÐµÑ Ñ€ÐµÑ”Ñтрації. Якщо ви вказали невірну адреÑу електронної пошти, ви можете увійти ще раз Ñ– змінити Ñ—Ñ—. @@ -970,6 +972,7 @@ passcode_invalid=Ðекоректний пароль. Спробуй ще раз twofa_failed_get_secret=Ðе вдалоÑÑ Ð¾Ñ‚Ñ€Ð¸Ð¼Ð°Ñ‚Ð¸ код. webauthn_register_key=Додати ключ безпеки +webauthn_nickname=ПÑевдонім webauthn_delete_key=Видалити ключ безпеки webauthn_delete_key_desc=Якщо ви видалите ключ безпеки, ви більше не зможете ввійти за його допомогою. Продовжити? webauthn_key_loss_warning=Якщо ви втратите ключі безпеки, ви втратите доÑтуп до Ñвого облікового запиÑу. @@ -1320,7 +1323,6 @@ editor.update=Оновити %s editor.delete=Видалити %s editor.patch=ЗаÑтоÑувати патч editor.patching=ЗаÑтоÑÑƒÐ²Ð°Ð½Ð½Ñ Ð²Ð¸Ð¿Ñ€Ð°Ð²Ð»ÐµÐ½ÑŒ: -editor.fail_to_apply_patch=`Ðе вдалоÑÑ Ð·Ð°ÑтоÑувати патч "%s"` editor.new_patch=Ðовий патч editor.commit_message_desc=Додати необов'Ñзковий розширений опиÑ… editor.signoff_desc=Додати «ПідпиÑано комітером» в кінці Ð¿Ð¾Ð²Ñ–Ð´Ð¾Ð¼Ð»ÐµÐ½Ð½Ñ ÐºÐ¾Ð¼Ñ–Ñ‚Ñƒ. @@ -1337,13 +1339,11 @@ editor.commit_email=Електронна пошта коміту editor.invalid_commit_email=ÐдреÑа електронної пошти Ð´Ð»Ñ ÐºÐ¾Ð¼Ñ–Ñ‚Ñƒ недійÑна. editor.file_is_a_symlink=`"%s" - це Ñимволічне поÑиланнÑ. Символічні поÑÐ¸Ð»Ð°Ð½Ð½Ñ Ð½Ðµ можна редагувати у веб-редакторі` editor.filename_is_a_directory=Ðазва файлу '%s' вже викориÑтовуєтьÑÑ Ñк назва каталогу у цьому Ñховищі. -editor.file_deleting_no_longer_exists=Видалений файл '%s' більше не Ñ–Ñнує в цьому Ñховищі. +editor.file_modifying_no_longer_exists=Редагований файл '%s' більше не Ñ–Ñнує в цьому Ñховищі. editor.file_changed_while_editing=ЗміÑÑ‚ файлу змінивÑÑ Ð· моменту початку редагуваннÑ. <a target="_blank" rel="noopener" href="%s"> ÐатиÑніть тут </a>, щоб переглÑнути що було змінено, або <strong>закомітьте зміни ще раз</strong>, щоб перепиÑати Ñ—Ñ…. editor.commit_empty_file_header=Закомітити порожній файл editor.commit_empty_file_text=Файл, Ñкий ви збираєтеÑÑ Ð·Ð°ÐºÐ¾Ð¼Ñ–Ñ‚Ð¸Ñ‚Ð¸, порожній. Продовжувати? editor.no_changes_to_show=Ðемає змін. -editor.fail_to_update_file=Ðе вдалоÑÑ Ð¾Ð½Ð¾Ð²Ð¸Ñ‚Ð¸/Ñтворити файл "%s". -editor.fail_to_update_file_summary=Помилка: editor.push_rejected_no_message=Зміну відхилено Ñервером без повідомленнÑ. Будь лаÑка, перевірте Git-хуки. editor.push_rejected=Зміну відхилено Ñервером. Будь лаÑка, перевірте Git-хуки. editor.push_rejected_summary=Повне Ð¿Ð¾Ð²Ñ–Ð´Ð¾Ð¼Ð»ÐµÐ½Ð½Ñ Ð¿Ñ€Ð¾ відмову: @@ -1353,9 +1353,14 @@ editor.upload_file_is_locked=Файл "%s" заблоковано %s. editor.upload_files_to_dir=`Завантажити файли до "%s"` editor.no_commit_to_branch=Ðе вдалоÑÑ Ð²Ð½ÐµÑти коміт безпоÑередньо до гілки, тому що: editor.require_signed_commit=Гілка вимагає підпиÑаного коміту +editor.revert=Повернути %s до: +editor.failed_to_commit=Ðе вдалоÑÑ Ð·Ð°Ñ„Ñ–ÐºÑувати зміни. +editor.failed_to_commit_summary=Помилка: + commits.desc=ПереглÑнути Ñ–Ñторію зміни коду. commits.commits=Коміти +commits.no_commits=Ðемає Ñпільних комітів. '%s' та '%s' мають різну Ñ–Ñторію. commits.nothing_to_compare=Ці гілки однакові. commits.search_branch=Ð¦Ñ Ð³Ñ–Ð»ÐºÐ° commits.search_all=УÑÑ– гілки @@ -1372,6 +1377,7 @@ commits.ssh_key_fingerprint=Відбиток ключа SSH commits.view_path=ПереглÑнути в Ñ–Ñторії commits.view_file_diff=ПереглÑнути зміни до цього файлу в цьому коміті +commit.operations=Дії commit.revert=Повернути до попереднього Ñтану commit.revert-header=Повернути: %s commit.revert-content=Виберіть гілку, до Ñкої хочете повернутиÑÑ: @@ -1446,6 +1452,7 @@ issues.new.clear_assignees=Прибрати виконавців issues.new.no_assignees=Ðемає виконавців issues.new.no_reviewers=Ðемає рецензентів issues.new.blocked_user=Ðе вдалоÑÑ Ñтворити задачу, тому що ви заблоковані влаÑником Ñховища. +issues.edit.blocked_user=Ðеможливо редагувати вміÑÑ‚, оÑкільки Ð²Ð°Ñ Ð·Ð°Ð±Ð»Ð¾ÐºÐ¾Ð²Ð°Ð½Ð¾ автором або влаÑником Ñховища. issues.choose.get_started=Розпочати issues.choose.open_external_link=Відкрити issues.choose.blank=Типово @@ -1463,6 +1470,7 @@ issues.label_templates.title=Завантажити визначений Ð½Ð°Ð±Ñ issues.label_templates.info=Ще немає міток. ÐатиÑніть 'Ðова мітка' або викориÑтовуйте попередньо визначений набір міток: issues.label_templates.helper=Оберіть набір міток issues.label_templates.use=ВикориÑтати набір міток +issues.label_templates.fail_to_load_file=Ðе вдалоÑÑ Ð·Ð°Ð²Ð°Ð½Ñ‚Ð°Ð¶Ð¸Ñ‚Ð¸ файл шаблона мітки '%s': %v issues.add_label=додано %s з міткою %s issues.add_labels=додано %s з мітками %s issues.remove_label=видалено %s з міткою %s @@ -1498,6 +1506,7 @@ issues.filter_project=Проєкт issues.filter_project_all=Ð’ÑÑ– проєкти issues.filter_project_none=Проєкт відÑутній issues.filter_assignee=Виконавець +issues.filter_assignee_no_assignee=Ðікому не приÑвоєно issues.filter_assignee_any_assignee=Призначено будь-кому issues.filter_poster=Ðвтор issues.filter_user_placeholder=Пошук кориÑтувачів @@ -1508,6 +1517,7 @@ issues.filter_type.all_pull_requests=УÑÑ– запити на Ð·Ð»Ð¸Ñ‚Ñ‚Ñ issues.filter_type.assigned_to_you=Призначене вам issues.filter_type.created_by_you=Створено вами issues.filter_type.mentioning_you=Ð’Ð°Ñ Ð·Ð³Ð°Ð´Ð°Ð½Ð¾ +issues.filter_type.review_requested=Запит на рецензію issues.filter_type.reviewed_by_you=Перевірено вами issues.filter_sort=Сортувати issues.filter_sort.latest=Ðайновіші @@ -1547,7 +1557,9 @@ issues.context.quote_reply=Цитувати відповідь issues.context.reference_issue=ПоÑÐ¸Ð»Ð°Ð½Ð½Ñ Ð² новій задачі issues.context.edit=Редагувати issues.context.delete=Видалити +issues.no_content=Ðемає опиÑу. issues.close=Закрити задачу +issues.comment_pull_merged_at=об'єднав(-ла) коміти %[1]s в %[2]s %[3]s issues.comment_manually_pull_merged_at=вручну об'єднав(-ла) коміти %[1]s в %[2]s %[3]s issues.close_comment_issue=Закрити з коментарем issues.reopen_issue=Відкрити знову @@ -1590,6 +1602,7 @@ issues.label_title=Ðазва мітки issues.label_description=ÐžÐ¿Ð¸Ñ Ð¼Ñ–Ñ‚ÐºÐ¸ issues.label_color=Колір issues.label_color_invalid=ÐедійÑний колір +issues.label_exclusive=ЕкÑклюзивно issues.label_archive=Мітка архіву issues.label_archived_filter=Показати архівовані мітки issues.label_archive_tooltip=Ðрхівовані мітки типово виключаютьÑÑ Ð· пропозицій під Ñ‡Ð°Ñ Ð¿Ð¾ÑˆÑƒÐºÑƒ за мітками. @@ -1781,7 +1794,13 @@ pulls.switch_comparison_type=Перемкнути тип порівнÑÐ½Ð½Ñ pulls.switch_head_and_base=ПомінÑти міÑцÑми оÑновну та базову гілку pulls.filter_branch=Фільтр по гілці pulls.show_all_commits=Показати вÑÑ– коміти +pulls.show_changes_since_your_last_review=Показати зміни піÑÐ»Ñ Ð²Ð°ÑˆÐ¾Ð³Ð¾ оÑтаннього відгуку +pulls.showing_only_single_commit=ВідображаютьÑÑ Ð»Ð¸ÑˆÐµ зміни коміту %[1]s +pulls.showing_specified_commit_range=ВідображаютьÑÑ Ð»Ð¸ÑˆÐµ зміни між %[1]s..%[2]s +pulls.select_commit_hold_shift_for_range=Виберіть коміт. ÐатиÑніть клавішу Shift + клацніть, щоб виділити діапазон +pulls.filter_changes_by_commit=Фільтр за комітом pulls.nothing_to_compare=Ці гілки однакові. Ðемає необхідноÑті Ñтворювати запитів на злиттÑ. +pulls.nothing_to_compare_have_tag=Виділена гілка або мітка ідентичні. pulls.nothing_to_compare_and_allow_empty_pr=Одинакові гілки. Цей PR буде порожнім. pulls.has_pull_request=`Запит Ð·Ð»Ð¸Ñ‚Ñ‚Ñ Ð´Ð»Ñ Ñ†Ð¸Ñ… гілок вже Ñ–Ñнує: <a href="%[1]s">%[2]s#%[3]d</a>` pulls.create=Створити запит на Ð·Ð»Ð¸Ñ‚Ñ‚Ñ @@ -1794,6 +1813,7 @@ pulls.tab_files=Змінені файли pulls.reopen_to_merge=Будь лаÑка, заново відкрийте цей запит щоб виконати злиттÑ. pulls.cant_reopen_deleted_branch=Цей запит не можна повторно відкрити, оÑкільки гілку видалено. pulls.merged=Злито +pulls.merged_success=Запит на Ð·Ð»Ð¸Ñ‚Ñ‚Ñ ÑƒÑпішно об'єднано Ñ– закрито pulls.closed=Запит на Ð·Ð»Ð¸Ñ‚Ñ‚Ñ Ð·Ð°ÐºÑ€Ð¸Ñ‚Ð¾ pulls.manually_merged=Ручне Ð·Ð»Ð¸Ñ‚Ñ‚Ñ pulls.merged_info_text=Гілку %s тепер можна видалити. @@ -1806,6 +1826,7 @@ pulls.remove_prefix=Видалити Ð¿Ñ€ÐµÑ„Ñ–ÐºÑ <strong>%s</strong> pulls.data_broken=Збій цього запиту на Ð·Ð»Ð¸Ñ‚Ñ‚Ñ Ñ‡ÐµÑ€ÐµÐ· відÑутніÑть інформації про форк. pulls.files_conflicted=Цей запит на Ð·Ð»Ð¸Ñ‚Ñ‚Ñ Ð¼Ð°Ñ” зміни, що конфліктують з цільовою гілкою. pulls.is_checking=Перевірка конфліктів об'Ñ”Ð´Ð½Ð°Ð½Ð½Ñ (merge) ... +pulls.is_ancestor=Цю гілку вже включено до цільової гілки. Ðема чого об'єднувати. pulls.required_status_check_failed=ДеÑкі необхідні перевірки виконані з помилками. pulls.required_status_check_missing=Декілька з необхідних перевірок відÑутні. pulls.required_status_check_administrator=Як адмініÑтратор ви вÑе одно можете об'єднати цей запит на злиттÑ. @@ -1873,6 +1894,9 @@ pulls.auto_merge_not_scheduled=Цей запит на Ð·Ð»Ð¸Ñ‚Ñ‚Ñ Ð½Ðµ план pulls.delete.title=Видалити цей запит на злиттÑ? +pulls.upstream_diverging_prompt_behind_1=Ð¦Ñ Ð³Ñ–Ð»ÐºÐ° на %[1]d коміт позаду %[2]s +pulls.upstream_diverging_prompt_behind_n=Ð¦Ñ Ð³Ñ–Ð»ÐºÐ° на %[1]d комітів позаду %[2]s +pulls.upstream_diverging_prompt_base_newer=Базова гілка %s має нові зміни pulls.upstream_diverging_merge_confirm=Хочете об’єднати "%[1]s" з "%[2]s"? pull.deleted_branch=(видалена):%s @@ -2252,6 +2276,10 @@ settings.event_pull_request_review=Запит на Ð·Ð»Ð¸Ñ‚Ñ‚Ñ Ñ€ÐµÑ†ÐµÐ½Ð·Ð¾Ð² settings.event_pull_request_review_desc=Запит на Ð·Ð»Ð¸Ñ‚Ñ‚Ñ Ð¿Ñ–Ð´Ñ‚Ð²ÐµÑ€Ð´Ð¶ÐµÐ½Ð¾, відхилено або прокоментовано. settings.event_pull_request_sync=Запит на Ð·Ð»Ð¸Ñ‚Ñ‚Ñ ÑинхронізуєтьÑÑ settings.event_pull_request_sync_desc=Запит до Ð·Ð»Ð¸Ñ‚Ñ‚Ñ Ñинхронізовано. +settings.event_header_workflow=Події робочого процеÑу +settings.event_workflow_run=Запущений робочий Ð¿Ñ€Ð¾Ñ†ÐµÑ +settings.event_workflow_run_desc=Запущений робочий Ð¿Ñ€Ð¾Ñ†ÐµÑ Gitea в черзі, в очікуванні, в процеÑÑ– Ð²Ð¸ÐºÐ¾Ð½Ð°Ð½Ð½Ñ Ð°Ð±Ð¾ завершений. +settings.event_workflow_job=Ð—Ð°Ð²Ð´Ð°Ð½Ð½Ñ Ñ€Ð¾Ð±Ð¾Ñ‡Ð¾Ð³Ð¾ процеÑу settings.event_package=Пакет settings.branch_filter=Фільтр гілок settings.authorization_header=Заголовок авторизації @@ -2590,6 +2618,7 @@ team_permission_desc=Права доÑтупу team_unit_desc=Дозволити доÑтуп до розділів репозиторію team_unit_disabled=(Вимкнено) +form.name_been_taken=Ðазва організації "%s" вже зайнÑта. form.name_reserved=Ðазву організації "%s" зарезервовано. form.name_pattern_not_allowed=Шаблон "%s" не допуÑкаєтьÑÑ Ð² назві організації. form.create_org_not_allowed=Вам не дозволено Ñтворювати організації. @@ -2611,15 +2640,28 @@ settings.visibility.private_shortname=Приватний settings.update_settings=Оновити Ð½Ð°Ð»Ð°ÑˆÑ‚ÑƒÐ²Ð°Ð½Ð½Ñ settings.update_setting_success=ÐÐ°Ð»Ð°ÑˆÑ‚ÑƒÐ²Ð°Ð½Ð½Ñ Ð¾Ñ€Ð³Ð°Ð½Ñ–Ð·Ð°Ñ†Ñ–Ñ— оновлені. -settings.change_orgname_prompt=Примітка: Зміна назви організації також змінить URL-адреÑу вашої організації та звільнить Ñтару назву. -settings.change_orgname_redirect_prompt=Стара назва буде перенаправлÑтиÑÑ Ð´Ð¾ тих пір, поки не буде заброньована. + +settings.rename=Перейменувати організацію +settings.rename_desc=Зміна назви організації також змінить URL адреÑу вашої організації Ñ– звільнить Ñтару назву. +settings.rename_success=Організацію %[1]s уÑпішно перейменована на %[2]. +settings.rename_no_change=Ðазва організації не змінилаÑÑ. +settings.rename_new_org_name=Ðазва нової організації +settings.rename_failed=Ðе вдалоÑÑ Ð¿ÐµÑ€ÐµÐ¹Ð¼ÐµÐ½ÑƒÐ²Ð°Ñ‚Ð¸ організацію через внутрішню помилку +settings.rename_notices_1=Цю операцію <strong>ÐЕМОЖЛИВО</strong> ÑкаÑувати. +settings.rename_notices_2=Стара назва буде перенаправлÑтиÑÑ Ð½Ð° нову, поки хтоÑÑŒ не викориÑтає Ñ—Ñ—. + settings.update_avatar_success=Ðватар організації оновлений. settings.delete=Видалити організацію settings.delete_account=Видалити цю організацію settings.delete_prompt=Організацію буде оÑтаточно видалено. Це <strong>ÐЕМОЖЛИВО</strong> ÑкаÑувати! +settings.name_confirm=Введіть назву організації Ð´Ð»Ñ Ð¿Ñ–Ð´Ñ‚Ð²ÐµÑ€Ð´Ð¶ÐµÐ½Ð½Ñ: +settings.delete_notices_1=Цю операцію <strong>ÐЕМОЖЛИВО</strong> ÑкаÑувати. +settings.delete_notices_2=Ð¦Ñ Ð¾Ð¿ÐµÑ€Ð°Ñ†Ñ–Ñ Ð½Ð°Ð·Ð°Ð²Ð¶Ð´Ð¸ видалить <strong>Ñховища</strong> <strong>%s</strong>, включно з кодом, задачами, коментарÑми, даними вікі та налаштуваннÑми Ñпівавторів. +settings.delete_notices_3=Ð¦Ñ Ð¾Ð¿ÐµÑ€Ð°Ñ†Ñ–Ñ Ð½Ð°Ð·Ð°Ð²Ð¶Ð´Ð¸ видалить вÑÑ– <strong>пакети</strong> <strong>%s</strong>. +settings.delete_notices_4=Ð¦Ñ Ð¾Ð¿ÐµÑ€Ð°Ñ†Ñ–Ñ Ð½Ð°Ð·Ð°Ð²Ð¶Ð´Ð¸ видалить вÑÑ– <strong>проєкти</strong> <strong>%s</strong>. settings.confirm_delete_account=Підтвердити Ð²Ð¸Ð´Ð°Ð»ÐµÐ½Ð½Ñ -settings.delete_org_title=Видалити організацію -settings.delete_org_desc=Ð¦Ñ Ð¾Ñ€Ð³Ð°Ð½Ñ–Ð·Ð°Ñ†Ñ–Ñ Ð±ÑƒÐ´Ðµ безповоротно видалена. Продовжити? +settings.delete_failed=Ðе вдалоÑÑ Ð²Ð¸Ð´Ð°Ð»Ð¸Ñ‚Ð¸ організацію через внутрішню помилку +settings.delete_successful=Організацію <b>%s</b> уÑпішно видалено. settings.hooks_desc=Додайте веб-хуки, Ñкі Ñпрацьовуватимуть Ð´Ð»Ñ <strong>вÑÑ–Ñ… Ñховищ</strong> у цій організації. settings.labels_desc=Додайте мітки, Ñкі можна викориÑтовувати у задачах Ð´Ð»Ñ <strong>уÑÑ–Ñ… Ñховищ</strong> у цій організації. @@ -2645,6 +2687,7 @@ teams.leave.detail=Покинути %s? teams.can_create_org_repo=Створити репозиторії teams.can_create_org_repo_helper=УчаÑники можуть Ñтворювати нові репозиторії в організації. Ðвтор отримає доÑтуп адмініÑтратора до нового репозиторію. teams.none_access=Ðемає доÑтупу +teams.none_access_helper=УчаÑники не можуть переглÑдати або виконувати будь-Ñкі інші дії з цією одиницею. Це не впливає на загальнодоÑтупні Ñховища. teams.general_access=Загальний доÑтуп teams.general_access_helper=Дозволи учаÑників будуть визначатиÑÑ Ð²Ñ–Ð´Ð¿Ð¾Ð²Ñ–Ð´Ð½Ð¾ до наведеної нижче таблиці дозволів. teams.read_access=Ð§Ð¸Ñ‚Ð°Ð½Ð½Ñ @@ -2673,6 +2716,7 @@ teams.remove_all_repos_title=Видалити вÑÑ– репозиторії ко teams.remove_all_repos_desc=Це видалить уÑÑ– репозиторії команди. teams.add_all_repos_title=Додати вÑÑ– репозиторії teams.add_all_repos_desc=Це додаÑть вÑÑ– репозиторії організації до команди. +teams.add_nonexistent_repo=Сховище, Ñке ви намагаєтеÑÑ Ð´Ð¾Ð´Ð°Ñ‚Ð¸, не Ñ–Ñнує, будь лаÑка, Ñтворіть його Ñпочатку. teams.add_duplicate_users=КориÑтувач уже Ñ” членом команди. teams.repos.none=Ð”Ð»Ñ ÐºÐ¾Ð¼Ð°Ð½Ð´Ð¸ немає доÑтупних репозиторіїв. teams.members.none=Ðемає членів в цій команді. @@ -2764,6 +2808,7 @@ dashboard.resync_all_hooks=Заново Ñинхронізувати хуки п dashboard.reinit_missing_repos=Заново ініціалізувати вÑÑ– відÑутні Ñховища Git'а, Ð´Ð»Ñ Ñких Ñ–Ñнують запиÑи dashboard.sync_external_users=Синхронізувати дані зовнішніх кориÑтувачів dashboard.cleanup_hook_task_table=ОчиÑтити таблицю hook_task +dashboard.cleanup_packages=ОчиÑтити заÑтарілі пакети dashboard.cleanup_actions=ÐžÑ‡Ð¸Ñ‰ÐµÐ½Ð½Ñ Ñ€ÐµÑурÑів проÑтрочених дій dashboard.server_uptime=Ð§Ð°Ñ Ñ€Ð¾Ð±Ð¾Ñ‚Ð¸ Ñервера dashboard.current_goroutine=Поточна кількіÑть Goroutines @@ -2795,11 +2840,15 @@ dashboard.total_gc_pause=Загальна пауза збирача ÑÐ¼Ñ–Ñ‚Ñ‚Ñ dashboard.last_gc_pause=ОÑÑ‚Ð°Ð½Ð½Ñ Ð¿Ð°ÑƒÐ·Ð° збирача ÑÐ¼Ñ–Ñ‚Ñ‚Ñ (GC) dashboard.gc_times=КількіÑть запуÑків збирача ÑÐ¼Ñ–Ñ‚Ñ‚Ñ (GC) dashboard.delete_old_actions=Видалити вÑÑ– Ñтарі дії з бази даних +dashboard.delete_old_actions.started=Ð’Ð¸Ð´Ð°Ð»ÐµÐ½Ð½Ñ Ð²ÑÑ–Ñ… Ñтарих дій з бази даних розпочато. dashboard.update_checker=Перевірка оновлень +dashboard.delete_old_system_notices=Видалити вÑÑ– Ñтарі ÑиÑтемні Ð¿Ð¾Ð²Ñ–Ð´Ð¾Ð¼Ð»ÐµÐ½Ð½Ñ Ð· бази даних dashboard.gc_lfs=Збір ÑÐ¼Ñ–Ñ‚Ñ‚Ñ Ð¼ÐµÑ‚Ð°-об'єктів LFS +dashboard.stop_endless_tasks=Зупинити неÑкінченні Ð·Ð°Ð²Ð´Ð°Ð½Ð½Ñ dashboard.cancel_abandoned_jobs=СкаÑувати покинуті Ð·Ð°Ð²Ð´Ð°Ð½Ð½Ñ dashboard.start_schedule_tasks=ЗапуÑк запланованих завдань dashboard.sync_branch.started=Розпочато Ñинхронізацію гілок +dashboard.sync_tag.started=Розпочато Ñинхронізацію міток dashboard.rebuild_issue_indexer=Перебудувати індекÑатор задач dashboard.sync_repo_licenses=Синхронізувати ліцензії Ñховища @@ -3193,6 +3242,7 @@ monitor.queue.settings.remove_all_items_done=УÑÑ– елементи черги notices.system_notice_list=Ð¡Ð¿Ð¾Ð²Ñ–Ñ‰ÐµÐ½Ð½Ñ ÑиÑтеми notices.view_detail_header=ПереглÑнути деталі Ð¿Ð¾Ð²Ñ–Ð´Ð¾Ð¼Ð»ÐµÐ½Ð½Ñ +notices.operations=Дії notices.select_all=Вибрати вÑе notices.deselect_all=СкаÑувати Ð²Ð¸Ð´Ñ–Ð»ÐµÐ½Ð½Ñ notices.inverse_selection=Інвертувати виділене @@ -3260,6 +3310,7 @@ seconds=%d Ñекунди minutes=%d хвилини hours=%d години days=%d дні +weeks=%d тижні(в) months=%d міÑÑці years=%d роки raw_seconds=Ñекунди @@ -3330,6 +3381,7 @@ details.license=Ð›Ñ–Ñ†ÐµÐ½Ð·Ñ–Ñ assets=РеÑурÑи versions=ВерÑÑ–Ñ— versions.view_all=ПереглÑнути вÑе +dependency.id=ID dependency.version=ВерÑÑ–Ñ search_in_external_registry=Шукати в %s alpine.registry=Ðалаштуйте цей реєÑтр, додавши URL у ваш файл <code>/etc/apk/repositories</code>: @@ -3469,6 +3521,8 @@ actions=Дії unit.desc=Керувати діÑми status.unknown=Ðевідомий +status.waiting=ÐžÑ‡Ñ–ÐºÑƒÐ²Ð°Ð½Ð½Ñ +status.running=ВиконуєтьÑÑ status.success=УÑпіх status.failure=Ðевдача status.cancelled=СкаÑовано @@ -3476,6 +3530,7 @@ status.skipped=Пропущено status.blocked=Заблоковано runners.status=Ð¡Ñ‚Ð°Ñ‚ÑƒÑ +runners.id=ID runners.name=Ðазва runners.owner_type=Тип runners.description=ÐžÐ¿Ð¸Ñ @@ -3498,23 +3553,36 @@ runs.all_workflows=Ð’ÑÑ– робочі процеÑи runs.commit=Коміт runs.scheduled=Заплановано runs.pushed_by=завантажено +runs.invalid_workflow_helper=Файл конфігурації робочих процеÑів недійÑний. Будь лаÑка, перевірте файл конфігурації: %s +runs.no_job_without_needs=Робочий Ð¿Ñ€Ð¾Ñ†ÐµÑ Ð¿Ð¾Ð²Ð¸Ð½ÐµÐ½ міÑтити принаймні одну задачу без залежноÑтей. runs.no_job=Робочий Ð¿Ñ€Ð¾Ñ†ÐµÑ Ð¿Ð¾Ð²Ð¸Ð½ÐµÐ½ міÑтити принаймні одну задачу runs.actor=Ðктор runs.status=Ð¡Ñ‚Ð°Ñ‚ÑƒÑ runs.actors_no_select=УÑÑ– актори runs.status_no_select=Ð’ÑÑ– ÑтатуÑи runs.no_results=Збігів немає. +runs.no_workflows=Робочих процеÑів наразі немає. +runs.no_workflows.quick_start=Ðе знаєте, Ñк почати з Gitea Дії? ДивітьÑÑ <a target="_blank" rel="noopener noreferrer" href="%s">поÑібник швидкого Ñтарту</a>. runs.no_workflows.documentation=Ð”Ð»Ñ Ð¾Ñ‚Ñ€Ð¸Ð¼Ð°Ð½Ð½Ñ Ð´Ð¾Ð´Ð°Ñ‚ÐºÐ¾Ð²Ð¾Ñ— інформації про Gitea Дії, переглÑньте <a target="_blank" rel="noopener noreferrer" href="%s">документацію</a>. +runs.no_runs=Робочий Ð¿Ñ€Ð¾Ñ†ÐµÑ Ñ‰Ðµ не виконувавÑÑ. runs.empty_commit_message=(порожнє Ð¿Ð¾Ð²Ñ–Ð´Ð¾Ð¼Ð»ÐµÐ½Ð½Ñ ÐºÐ¾Ð¼Ñ–Ñ‚Ñƒ) +runs.expire_log_message=Журнали були очищені, тому що вони були занадто Ñтарі. +runs.delete=Видалити запущений робочий Ð¿Ñ€Ð¾Ñ†ÐµÑ +runs.delete.description=Ви впевнені, що хочете оÑтаточно видалити цей робочий процеÑ? Цю дію неможливо ÑкаÑувати. +runs.not_done=Ð’Ð¸ÐºÐ¾Ð½Ð°Ð½Ð½Ñ Ñ†ÑŒÐ¾Ð³Ð¾ робочого процеÑу не завершено. runs.view_workflow_file=ПереглÑд файлу робочого процеÑу workflow.disable=Вимкнути робочий Ð¿Ñ€Ð¾Ñ†ÐµÑ workflow.disable_success=Робочий Ð¿Ñ€Ð¾Ñ†ÐµÑ '%s' уÑпішно вимкнено. workflow.enable=Увімкнути робочий Ð¿Ñ€Ð¾Ñ†ÐµÑ workflow.enable_success=Робочий Ð¿Ñ€Ð¾Ñ†ÐµÑ '%s' уÑпішно ввімкнено. +workflow.disabled=Робочий Ð¿Ñ€Ð¾Ñ†ÐµÑ Ð²Ð¸Ð¼ÐºÐ½ÐµÐ½Ð¸Ð¹. workflow.run=ЗапуÑтити робочий Ð¿Ñ€Ð¾Ñ†ÐµÑ workflow.not_found=Робочий Ð¿Ñ€Ð¾Ñ†ÐµÑ '%s' не знайдено. +workflow.run_success=Робочий Ð¿Ñ€Ð¾Ñ†ÐµÑ '%s' завершивÑÑ ÑƒÑпішно. workflow.from_ref=ВикориÑтати робочий Ð¿Ñ€Ð¾Ñ†ÐµÑ Ð· +workflow.has_workflow_dispatch=Цей робочий Ð¿Ñ€Ð¾Ñ†ÐµÑ Ð¼Ð°Ñ” тригер події workflow_dispatch. +workflow.has_no_workflow_dispatch=Робочий Ð¿Ñ€Ð¾Ñ†ÐµÑ â€œ%s†не має тригера події workflow_dispatch. variables=Змінні diff --git a/options/locale/locale_zh-CN.ini b/options/locale/locale_zh-CN.ini index 7d37661725..152cfb3da1 100644 --- a/options/locale/locale_zh-CN.ini +++ b/options/locale/locale_zh-CN.ini @@ -1354,7 +1354,6 @@ editor.update=æ›´æ–° %s editor.delete=åˆ é™¤ %s editor.patch=åº”ç”¨è¡¥ä¸ editor.patching=打补ä¸ï¼š -editor.fail_to_apply_patch=æ— æ³•åº”ç”¨è¡¥ä¸ã€Œ%s〠editor.new_patch=æ–°è¡¥ä¸ editor.commit_message_desc=æ·»åŠ ä¸€ä¸ªå¯é€‰çš„æ‰©å±•æè¿°... editor.signoff_desc=在æäº¤æ—¥å¿—æ¶ˆæ¯æœ«å°¾æ·»åŠ ç¾ç½²äººä¿¡æ¯ã€‚ @@ -1374,8 +1373,6 @@ editor.branch_already_exists=æ¤ä»“库已å˜åœ¨å为「%sã€çš„分支。 editor.directory_is_a_file=目录å「%sã€å·²ä½œä¸ºæ–‡ä»¶å在æ¤ä»“库ä¸å˜åœ¨ã€‚ editor.file_is_a_symlink=`「%sã€æ˜¯ä¸€ä¸ªç¬¦å·é“¾æŽ¥ï¼Œæ— 法在 Web 编辑器ä¸ç¼–辑` editor.filename_is_a_directory=文件å「%sã€å·²ä½œä¸ºç›®å½•å在æ¤ä»“库ä¸å˜åœ¨ã€‚ -editor.file_editing_no_longer_exists=æ£åœ¨ç¼–辑的文件「%sã€å·²ä¸å˜åœ¨äºŽæ¤ä»“库。 -editor.file_deleting_no_longer_exists=æ£åœ¨åˆ 除的文件「%sã€å·²ä¸å˜åœ¨äºŽæ¤ä»“库。 editor.file_changed_while_editing=文件内容在您进行编辑时已ç»å‘生å˜åŠ¨ã€‚<a target="_blank" rel="noopener noreferrer" href="%s">å•击æ¤å¤„</a> 查看å˜åŠ¨çš„å…·ä½“å†…å®¹ï¼Œæˆ–è€… <strong>冿¬¡æäº¤</strong> 覆盖已å‘生的å˜åŠ¨ã€‚ editor.file_already_exists=æ¤ä»“库已ç»å˜åœ¨å为「%sã€çš„æ–‡ä»¶ã€‚ editor.commit_id_not_matching=æäº¤ ID 与您开始编辑时的 ID ä¸åŒ¹é…。请æäº¤åˆ°è¡¥ä¸åˆ†æ”¯ç„¶åŽåˆå¹¶ã€‚ @@ -1383,8 +1380,6 @@ editor.push_out_of_date=推é€ä¼¼ä¹Žå·²ç»è¿‡æ—¶ã€‚ editor.commit_empty_file_header=æäº¤ä¸€ä¸ªç©ºæ–‡ä»¶ editor.commit_empty_file_text=æ‚¨è¦æäº¤çš„æ–‡ä»¶æ˜¯ç©ºçš„ï¼Œç»§ç»å—? editor.no_changes_to_show=没有å¯ä»¥æ˜¾ç¤ºçš„å˜æ›´ã€‚ -editor.fail_to_update_file=æ›´æ–°/创建文件「%sã€å¤±è´¥ã€‚ -editor.fail_to_update_file_summary=错误信æ¯ï¼š editor.push_rejected_no_message=æ¤ä¿®æ”¹è¢«æœåŠ¡å™¨æ‹’ç»å¹¶ä¸”没有å馈消æ¯ã€‚请检查 Git é’©å。 editor.push_rejected=æ¤ä¿®æ”¹è¢«æœåŠ¡å™¨æ‹’ç»ã€‚请检查 Git é’©å。 editor.push_rejected_summary=详细拒ç»ä¿¡æ¯ï¼š @@ -1399,6 +1394,7 @@ editor.require_signed_commit=分支需è¦ç¾åæäº¤ editor.cherry_pick=拣选æäº¤ %s 到: editor.revert=å°† %s 还原到: + commits.desc=æµè§ˆä»£ç ä¿®æ”¹åŽ†å² commits.commits=æ¬¡ä»£ç æäº¤ commits.no_commits=没有共åŒçš„æäº¤ã€‚「%sã€å’Œã€Œ%sã€çš„历å²å®Œå…¨ä¸åŒã€‚ @@ -2831,15 +2827,13 @@ settings.visibility.private_shortname=ç§æœ‰ settings.update_settings=更新组织设置 settings.update_setting_success=组织设置已更新。 -settings.change_orgname_prompt=注æ„:更改组织åç§°åŒæ—¶ä¼šæ›´æ”¹ç»„织的 URL 地å€å¹¶é‡Šæ”¾æ—§çš„å称。 -settings.change_orgname_redirect_prompt=在被人使用å‰ï¼Œæ—§ç”¨æˆ·å将会被é‡å®šå‘。 + + settings.update_avatar_success=组织头åƒå·²ç»æ›´æ–°ã€‚ settings.delete=åˆ é™¤ç»„ç»‡ settings.delete_account=åˆ é™¤å½“å‰ç»„织 settings.delete_prompt=åˆ é™¤æ“作会永久清除该组织的信æ¯ï¼Œå¹¶ä¸” <strong>ä¸å¯æ¢å¤</strong>ï¼ settings.confirm_delete_account=ç¡®è®¤åˆ é™¤ç»„ç»‡ -settings.delete_org_title=åˆ é™¤ç»„ç»‡ -settings.delete_org_desc=æ¤ç»„ç»‡å°†ä¼šæ°¸ä¹…åˆ é™¤ï¼Œç¡®è®¤ç»§ç»å—? settings.hooks_desc=在æ¤å¤„æ·»åŠ çš„ Web é’©å将会应用到该组织下的 <strong>所有仓库</strong>。 settings.labels_desc=æ·»åŠ èƒ½å¤Ÿè¢«è¯¥ç»„ç»‡ä¸‹çš„ <strong>所有仓库</strong> 的工å•ä½¿ç”¨çš„æ ‡ç¾ã€‚ diff --git a/options/locale/locale_zh-HK.ini b/options/locale/locale_zh-HK.ini index 2874da3170..77a5c0abec 100644 --- a/options/locale/locale_zh-HK.ini +++ b/options/locale/locale_zh-HK.ini @@ -374,6 +374,7 @@ editor.create_new_branch=建立 <strong>新的分支</strong> ç‚ºæ¤æäº¤å’Œé–‹ editor.cancel=å–æ¶ˆ editor.no_changes_to_show=沒有å¯ä»¥é¡¯ç¤ºçš„變更。 + commits.commits=次程å¼ç¢¼æäº¤ commits.author=作者 commits.message=備註 @@ -656,10 +657,11 @@ settings.visibility.private_shortname=ç§æœ‰åº« settings.update_settings=更新組織è¨å®š settings.update_setting_success=組織è¨å®šå·²æ›´æ–°ã€‚ + + settings.delete=刪除組織 settings.delete_account=刪除當å‰çµ„ç¹” settings.confirm_delete_account=確èªåˆªé™¤çµ„ç¹” -settings.delete_org_title=刪除組織 settings.hooks_desc=新增 webhooks 將觸發在這個組織下 <strong>全部的儲å˜åº«</strong> 。 diff --git a/options/locale/locale_zh-TW.ini b/options/locale/locale_zh-TW.ini index a52e147415..b1881d066a 100644 --- a/options/locale/locale_zh-TW.ini +++ b/options/locale/locale_zh-TW.ini @@ -1318,7 +1318,6 @@ editor.update=æ›´æ–° %s editor.delete=刪除 %s editor.patch=套用 Patch editor.patching=æ£åœ¨ Patch: -editor.fail_to_apply_patch=無法套用 Patch「%s〠editor.new_patch=新增 Patch editor.commit_message_desc=(é¸ç”¨) åŠ å…¥è©³ç´°èªªæ˜Ž... editor.signoff_desc=在æäº¤è¨Šæ¯åº•éƒ¨åŠ å…¥æäº¤è€…的「Signed-off-byã€è³‡è¨Šã€‚ @@ -1336,8 +1335,6 @@ editor.branch_already_exists=æ¤å„²å˜åº«å·²æœ‰å為「%sã€çš„分支。 editor.directory_is_a_file=目錄å稱「%sã€å·²è¢«æ¤å„²å˜åº«çš„æª”案使用。 editor.file_is_a_symlink=`"%s" 是一個符號連çµã€‚符號連çµç„¡æ³•在網é 編輯器ä¸ç·¨è¼¯` editor.filename_is_a_directory=檔å「%sã€å·²è¢«æ¤å„²å˜åº«çš„目錄å稱使用。 -editor.file_editing_no_longer_exists=æ£è¦ç·¨è¼¯çš„æª”案「%sã€å·²ä¸å˜åœ¨æ¤å„²å˜åº«ä¸ã€‚ -editor.file_deleting_no_longer_exists=æ£è¦åˆªé™¤çš„æª”案「%sã€å·²ä¸å˜åœ¨æ¤å„²å˜åº«ä¸ã€‚ editor.file_changed_while_editing=檔案內容在您編輯的途ä¸å·²è¢«è®Šæ›´ã€‚<a target="_blank" rel="noopener noreferrer" href="%s">按一下æ¤è™•</a>查看更動的地方或<strong>冿¬¡æäº¤</strong>以覆蓋這些變更。 editor.file_already_exists=æ¤å„²å˜åº«å·²æœ‰å為「%sã€çš„æª”案。 editor.commit_id_not_matching=æäº¤ ID 與您開始編輯時的 ID ä¸åŒ¹é…。請æäº¤åˆ°ä¸€å€‹è£œä¸åˆ†æ”¯ç„¶å¾Œåˆä½µã€‚ @@ -1345,8 +1342,6 @@ editor.push_out_of_date=推é€ä¼¼ä¹Žå·²éŽæ™‚。 editor.commit_empty_file_header=æäº¤ç©ºç™½æª”案 editor.commit_empty_file_text=ä½ æº–å‚™æäº¤çš„æª”案是空白的,是å¦ç¹¼çºŒï¼Ÿ editor.no_changes_to_show=沒有å¯ä»¥é¡¯ç¤ºçš„變更。 -editor.fail_to_update_file=æ›´æ–°/建立檔案「%sã€å¤±æ•—。 -editor.fail_to_update_file_summary=錯誤訊æ¯: editor.push_rejected_no_message=該變更被伺æœå™¨æ‹’絕但未æä¾›å…¶ä»–資訊。請檢查 Git Hook。 editor.push_rejected=該變更被伺æœå™¨æ‹’絕。請檢查 Git Hook。 editor.push_rejected_summary=完整的拒絕訊æ¯: @@ -1361,6 +1356,7 @@ editor.require_signed_commit=分支僅接å—經簽署的æäº¤ editor.cherry_pick=Cherry-pick %s 到: editor.revert=還原 %s 到: + commits.desc=ç€è¦½åŽŸå§‹ç¢¼ä¿®æ”¹æ·ç¨‹ã€‚ commits.commits=次程å¼ç¢¼æäº¤ commits.no_commits=沒有共åŒçš„æäº¤ã€‚「%sã€å’Œã€Œ%sã€çš„æ·å²å®Œå…¨ä¸åŒã€‚ @@ -2756,15 +2752,13 @@ settings.visibility.private_shortname=ç§æœ‰ settings.update_settings=æ›´æ–°è¨å®š settings.update_setting_success=組織è¨å®šå·²æ›´æ–°ã€‚ -settings.change_orgname_prompt=注æ„:更改組織åç¨±å°‡åŒæ™‚更改組織的 URL 並釋放舊å稱。 -settings.change_orgname_redirect_prompt=舊的åç¨±è¢«é ˜ç”¨å‰ï¼Œæœƒé‡æ–°å°Žå‘æ–°å稱。 + + settings.update_avatar_success=已更新組織的大é 貼。 settings.delete=刪除組織 settings.delete_account=刪除這個組織 settings.delete_prompt=該組織將被永久刪除。æ¤å‹•作<strong>ä¸å¯</strong>é‚„åŽŸï¼ settings.confirm_delete_account=確èªåˆªé™¤çµ„ç¹” -settings.delete_org_title=刪除組織 -settings.delete_org_desc=å³å°‡æ°¸ä¹…刪除這個組織,是å¦ç¹¼çºŒï¼Ÿ settings.hooks_desc=æ¤çµ„織下的<strong>所有å˜å„²åº«</strong>éƒ½æœƒè§¸ç™¼åœ¨æ¤æ–°å¢žçš„ Webhook。 settings.labels_desc=在æ¤è™•新增的標籤å¯ç”¨æ–¼æ¤çµ„織下的<strong>所有儲å˜åº«</strong>。 diff --git a/package-lock.json b/package-lock.json index 59ce5b33e0..d48702d31e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -30,7 +30,7 @@ "esbuild-loader": "4.3.0", "escape-goat": "4.0.0", "fast-glob": "3.3.3", - "htmx.org": "2.0.4", + "htmx.org": "2.0.5", "idiomorph": "0.7.3", "jquery": "3.7.1", "katex": "0.16.22", @@ -8233,9 +8233,9 @@ } }, "node_modules/htmx.org": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/htmx.org/-/htmx.org-2.0.4.tgz", - "integrity": "sha512-HLxMCdfXDOJirs3vBZl/ZLoY+c7PfM4Ahr2Ad4YXh6d22T5ltbTXFFkpx9Tgb2vvmWFMbIc3LqN2ToNkZJvyYQ==", + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/htmx.org/-/htmx.org-2.0.5.tgz", + "integrity": "sha512-ocgvtHCShWFW0DvSV1NbJC7Y5EzUMy2eo5zeWvGj2Ac4LOr7sv9YKg4jzCZJdXN21fXACmCViwKSy+cm6i2dWQ==", "license": "0BSD" }, "node_modules/iconv-lite": { diff --git a/package.json b/package.json index 4faf34900a..3dab385c6e 100644 --- a/package.json +++ b/package.json @@ -29,7 +29,7 @@ "esbuild-loader": "4.3.0", "escape-goat": "4.0.0", "fast-glob": "3.3.3", - "htmx.org": "2.0.4", + "htmx.org": "2.0.5", "idiomorph": "0.7.3", "jquery": "3.7.1", "katex": "0.16.22", diff --git a/routers/api/packages/api.go b/routers/api/packages/api.go index df5897e45e..f65c4b99ff 100644 --- a/routers/api/packages/api.go +++ b/routers/api/packages/api.go @@ -5,8 +5,6 @@ package packages import ( "net/http" - "regexp" - "strings" auth_model "code.gitea.io/gitea/models/auth" "code.gitea.io/gitea/models/perm" @@ -282,42 +280,10 @@ func CommonRoutes() *web.Router { }) }) }, reqPackageAccess(perm.AccessModeRead)) - r.Group("/conda", func() { - var ( - downloadPattern = regexp.MustCompile(`\A(.+/)?(.+)/((?:[^/]+(?:\.tar\.bz2|\.conda))|(?:current_)?repodata\.json(?:\.bz2)?)\z`) - uploadPattern = regexp.MustCompile(`\A(.+/)?([^/]+(?:\.tar\.bz2|\.conda))\z`) - ) - - r.Get("/*", func(ctx *context.Context) { - m := downloadPattern.FindStringSubmatch(ctx.PathParam("*")) - if len(m) == 0 { - ctx.Status(http.StatusNotFound) - return - } - - ctx.SetPathParam("channel", strings.TrimSuffix(m[1], "/")) - ctx.SetPathParam("architecture", m[2]) - ctx.SetPathParam("filename", m[3]) - - switch m[3] { - case "repodata.json", "repodata.json.bz2", "current_repodata.json", "current_repodata.json.bz2": - conda.EnumeratePackages(ctx) - default: - conda.DownloadPackageFile(ctx) - } - }) - r.Put("/*", reqPackageAccess(perm.AccessModeWrite), func(ctx *context.Context) { - m := uploadPattern.FindStringSubmatch(ctx.PathParam("*")) - if len(m) == 0 { - ctx.Status(http.StatusNotFound) - return - } - - ctx.SetPathParam("channel", strings.TrimSuffix(m[1], "/")) - ctx.SetPathParam("filename", m[2]) - - conda.UploadPackageFile(ctx) - }) + r.PathGroup("/conda/*", func(g *web.RouterPathGroup) { + g.MatchPath("GET", "/<architecture>/<filename>", conda.ListOrGetPackages) + g.MatchPath("GET", "/<channel:*>/<architecture>/<filename>", conda.ListOrGetPackages) + g.MatchPath("PUT", "/<channel:*>/<filename>", reqPackageAccess(perm.AccessModeWrite), conda.UploadPackageFile) }, reqPackageAccess(perm.AccessModeRead)) r.Group("/cran", func() { r.Group("/src", func() { @@ -358,60 +324,15 @@ func CommonRoutes() *web.Router { }, reqPackageAccess(perm.AccessModeRead)) r.Group("/go", func() { r.Put("/upload", reqPackageAccess(perm.AccessModeWrite), goproxy.UploadPackage) - r.Get("/sumdb/sum.golang.org/supported", func(ctx *context.Context) { - ctx.Status(http.StatusNotFound) - }) + r.Get("/sumdb/sum.golang.org/supported", http.NotFound) - // Manual mapping of routes because the package name contains slashes which chi does not support // https://go.dev/ref/mod#goproxy-protocol - r.Get("/*", func(ctx *context.Context) { - path := ctx.PathParam("*") - - if strings.HasSuffix(path, "/@latest") { - ctx.SetPathParam("name", path[:len(path)-len("/@latest")]) - ctx.SetPathParam("version", "latest") - - goproxy.PackageVersionMetadata(ctx) - return - } - - parts := strings.SplitN(path, "/@v/", 2) - if len(parts) != 2 { - ctx.Status(http.StatusNotFound) - return - } - - ctx.SetPathParam("name", parts[0]) - - // <package/name>/@v/list - if parts[1] == "list" { - goproxy.EnumeratePackageVersions(ctx) - return - } - - // <package/name>/@v/<version>.zip - if strings.HasSuffix(parts[1], ".zip") { - ctx.SetPathParam("version", parts[1][:len(parts[1])-len(".zip")]) - - goproxy.DownloadPackageFile(ctx) - return - } - // <package/name>/@v/<version>.info - if strings.HasSuffix(parts[1], ".info") { - ctx.SetPathParam("version", parts[1][:len(parts[1])-len(".info")]) - - goproxy.PackageVersionMetadata(ctx) - return - } - // <package/name>/@v/<version>.mod - if strings.HasSuffix(parts[1], ".mod") { - ctx.SetPathParam("version", parts[1][:len(parts[1])-len(".mod")]) - - goproxy.PackageVersionGoModContent(ctx) - return - } - - ctx.Status(http.StatusNotFound) + r.PathGroup("/*", func(g *web.RouterPathGroup) { + g.MatchPath("GET", "/<name:*>/@<version:latest>", goproxy.PackageVersionMetadata) + g.MatchPath("GET", "/<name:*>/@v/list", goproxy.EnumeratePackageVersions) + g.MatchPath("GET", "/<name:*>/@v/<version>.zip", goproxy.DownloadPackageFile) + g.MatchPath("GET", "/<name:*>/@v/<version>.info", goproxy.PackageVersionMetadata) + g.MatchPath("GET", "/<name:*>/@v/<version>.mod", goproxy.PackageVersionGoModContent) }) }, reqPackageAccess(perm.AccessModeRead)) r.Group("/generic", func() { @@ -532,82 +453,24 @@ func CommonRoutes() *web.Router { }) }) }, reqPackageAccess(perm.AccessModeRead)) + r.Group("/pypi", func() { r.Post("/", reqPackageAccess(perm.AccessModeWrite), pypi.UploadPackageFile) r.Get("/files/{id}/{version}/{filename}", pypi.DownloadPackageFile) r.Get("/simple/{id}", pypi.PackageMetadata) }, reqPackageAccess(perm.AccessModeRead)) - r.Group("/rpm", func() { - r.Group("/repository.key", func() { - r.Head("", rpm.GetRepositoryKey) - r.Get("", rpm.GetRepositoryKey) - }) - var ( - repoPattern = regexp.MustCompile(`\A(.*?)\.repo\z`) - uploadPattern = regexp.MustCompile(`\A(.*?)/upload\z`) - filePattern = regexp.MustCompile(`\A(.*?)/package/([^/]+)/([^/]+)/([^/]+)(?:/([^/]+\.rpm)|)\z`) - repoFilePattern = regexp.MustCompile(`\A(.*?)/repodata/([^/]+)\z`) - ) - - r.Methods("HEAD,GET,PUT,DELETE", "*", func(ctx *context.Context) { - path := ctx.PathParam("*") - isHead := ctx.Req.Method == http.MethodHead - isGetHead := ctx.Req.Method == http.MethodHead || ctx.Req.Method == http.MethodGet - isPut := ctx.Req.Method == http.MethodPut - isDelete := ctx.Req.Method == http.MethodDelete - - m := repoPattern.FindStringSubmatch(path) - if len(m) == 2 && isGetHead { - ctx.SetPathParam("group", strings.Trim(m[1], "/")) - rpm.GetRepositoryConfig(ctx) - return - } - - m = repoFilePattern.FindStringSubmatch(path) - if len(m) == 3 && isGetHead { - ctx.SetPathParam("group", strings.Trim(m[1], "/")) - ctx.SetPathParam("filename", m[2]) - if isHead { - rpm.CheckRepositoryFileExistence(ctx) - } else { - rpm.GetRepositoryFile(ctx) - } - return - } - - m = uploadPattern.FindStringSubmatch(path) - if len(m) == 2 && isPut { - reqPackageAccess(perm.AccessModeWrite)(ctx) - if ctx.Written() { - return - } - ctx.SetPathParam("group", strings.Trim(m[1], "/")) - rpm.UploadPackageFile(ctx) - return - } - - m = filePattern.FindStringSubmatch(path) - if len(m) == 6 && (isGetHead || isDelete) { - ctx.SetPathParam("group", strings.Trim(m[1], "/")) - ctx.SetPathParam("name", m[2]) - ctx.SetPathParam("version", m[3]) - ctx.SetPathParam("architecture", m[4]) - if isGetHead { - rpm.DownloadPackageFile(ctx) - } else { - reqPackageAccess(perm.AccessModeWrite)(ctx) - if ctx.Written() { - return - } - rpm.DeletePackageFile(ctx) - } - return - } - - ctx.Status(http.StatusNotFound) - }) + r.Methods("HEAD,GET", "/rpm.repo", reqPackageAccess(perm.AccessModeRead), rpm.GetRepositoryConfig) + r.PathGroup("/rpm/*", func(g *web.RouterPathGroup) { + g.MatchPath("HEAD,GET", "/repository.key", rpm.GetRepositoryKey) + g.MatchPath("HEAD,GET", "/<group:*>.repo", rpm.GetRepositoryConfig) + g.MatchPath("HEAD", "/<group:*>/repodata/<filename>", rpm.CheckRepositoryFileExistence) + g.MatchPath("GET", "/<group:*>/repodata/<filename>", rpm.GetRepositoryFile) + g.MatchPath("PUT", "/<group:*>/upload", reqPackageAccess(perm.AccessModeWrite), rpm.UploadPackageFile) + g.MatchPath("HEAD,GET", "/<group:*>/package/<name>/<version>/<architecture>", rpm.DownloadPackageFile) + g.MatchPath("DELETE", "/<group:*>/package/<name>/<version>/<architecture>", reqPackageAccess(perm.AccessModeWrite), rpm.DeletePackageFile) }, reqPackageAccess(perm.AccessModeRead)) + r.Group("/rubygems", func() { r.Get("/specs.4.8.gz", rubygems.EnumeratePackages) r.Get("/latest_specs.4.8.gz", rubygems.EnumeratePackagesLatest) @@ -621,6 +484,7 @@ func CommonRoutes() *web.Router { r.Delete("/yank", rubygems.DeletePackage) }, reqPackageAccess(perm.AccessModeWrite)) }, reqPackageAccess(perm.AccessModeRead)) + r.Group("/swift", func() { r.Group("", func() { // Needs to be unauthenticated. r.Post("", swift.CheckAuthenticate) @@ -632,31 +496,12 @@ func CommonRoutes() *web.Router { r.Get("", swift.EnumeratePackageVersions) r.Get(".json", swift.EnumeratePackageVersions) }, swift.CheckAcceptMediaType(swift.AcceptJSON)) - r.Group("/{version}", func() { - r.Get("/Package.swift", swift.CheckAcceptMediaType(swift.AcceptSwift), swift.DownloadManifest) - r.Put("", reqPackageAccess(perm.AccessModeWrite), swift.CheckAcceptMediaType(swift.AcceptJSON), swift.UploadPackageFile) - r.Get("", func(ctx *context.Context) { - // Can't use normal routes here: https://github.com/go-chi/chi/issues/781 - - version := ctx.PathParam("version") - if strings.HasSuffix(version, ".zip") { - swift.CheckAcceptMediaType(swift.AcceptZip)(ctx) - if ctx.Written() { - return - } - ctx.SetPathParam("version", version[:len(version)-4]) - swift.DownloadPackageFile(ctx) - } else { - swift.CheckAcceptMediaType(swift.AcceptJSON)(ctx) - if ctx.Written() { - return - } - if strings.HasSuffix(version, ".json") { - ctx.SetPathParam("version", version[:len(version)-5]) - } - swift.PackageVersionMetadata(ctx) - } - }) + r.PathGroup("/*", func(g *web.RouterPathGroup) { + g.MatchPath("GET", "/<version>.json", swift.CheckAcceptMediaType(swift.AcceptJSON), swift.PackageVersionMetadata) + g.MatchPath("GET", "/<version>.zip", swift.CheckAcceptMediaType(swift.AcceptZip), swift.DownloadPackageFile) + g.MatchPath("GET", "/<version>/Package.swift", swift.CheckAcceptMediaType(swift.AcceptSwift), swift.DownloadManifest) + g.MatchPath("GET", "/<version>", swift.CheckAcceptMediaType(swift.AcceptJSON), swift.PackageVersionMetadata) + g.MatchPath("PUT", "/<version>", reqPackageAccess(perm.AccessModeWrite), swift.CheckAcceptMediaType(swift.AcceptJSON), swift.UploadPackageFile) }) }) r.Get("/identifiers", swift.CheckAcceptMediaType(swift.AcceptJSON), swift.LookupPackageIdentifiers) @@ -705,18 +550,13 @@ func ContainerRoutes() *web.Router { r.PathGroup("/*", func(g *web.RouterPathGroup) { g.MatchPath("POST", "/<image:*>/blobs/uploads", reqPackageAccess(perm.AccessModeWrite), container.VerifyImageName, container.PostBlobsUploads) g.MatchPath("GET", "/<image:*>/tags/list", container.VerifyImageName, container.GetTagsList) - g.MatchPath("GET,PATCH,PUT,DELETE", `/<image:*>/blobs/uploads/<uuid:[-.=\w]+>`, reqPackageAccess(perm.AccessModeWrite), container.VerifyImageName, func(ctx *context.Context) { - switch ctx.Req.Method { - case http.MethodGet: - container.GetBlobsUpload(ctx) - case http.MethodPatch: - container.PatchBlobsUpload(ctx) - case http.MethodPut: - container.PutBlobsUpload(ctx) - default: /* DELETE */ - container.DeleteBlobsUpload(ctx) - } - }) + + patternBlobsUploadsUUID := g.PatternRegexp(`/<image:*>/blobs/uploads/<uuid:[-.=\w]+>`, reqPackageAccess(perm.AccessModeWrite), container.VerifyImageName) + g.MatchPattern("GET", patternBlobsUploadsUUID, container.GetBlobsUpload) + g.MatchPattern("PATCH", patternBlobsUploadsUUID, container.PatchBlobsUpload) + g.MatchPattern("PUT", patternBlobsUploadsUUID, container.PutBlobsUpload) + g.MatchPattern("DELETE", patternBlobsUploadsUUID, container.DeleteBlobsUpload) + g.MatchPath("HEAD", `/<image:*>/blobs/<digest>`, container.VerifyImageName, container.HeadBlob) g.MatchPath("GET", `/<image:*>/blobs/<digest>`, container.VerifyImageName, container.GetBlob) g.MatchPath("DELETE", `/<image:*>/blobs/<digest>`, container.VerifyImageName, reqPackageAccess(perm.AccessModeWrite), container.DeleteBlob) diff --git a/routers/api/packages/conda/conda.go b/routers/api/packages/conda/conda.go index fe7542dd18..cfe069d6db 100644 --- a/routers/api/packages/conda/conda.go +++ b/routers/api/packages/conda/conda.go @@ -36,6 +36,24 @@ func apiError(ctx *context.Context, status int, obj any) { }) } +func isCondaPackageFileName(filename string) bool { + return strings.HasSuffix(filename, ".tar.bz2") || strings.HasSuffix(filename, ".conda") +} + +func ListOrGetPackages(ctx *context.Context) { + filename := ctx.PathParam("filename") + switch filename { + case "repodata.json", "repodata.json.bz2", "current_repodata.json", "current_repodata.json.bz2": + EnumeratePackages(ctx) + return + } + if isCondaPackageFileName(filename) { + DownloadPackageFile(ctx) + return + } + ctx.NotFound(nil) +} + func EnumeratePackages(ctx *context.Context) { type Info struct { Subdir string `json:"subdir"` @@ -174,6 +192,12 @@ func EnumeratePackages(ctx *context.Context) { } func UploadPackageFile(ctx *context.Context) { + filename := ctx.PathParam("filename") + if !isCondaPackageFileName(filename) { + apiError(ctx, http.StatusBadRequest, nil) + return + } + upload, needToClose, err := ctx.UploadStream() if err != nil { apiError(ctx, http.StatusInternalServerError, err) @@ -191,7 +215,7 @@ func UploadPackageFile(ctx *context.Context) { defer buf.Close() var pck *conda_module.Package - if strings.HasSuffix(strings.ToLower(ctx.PathParam("filename")), ".tar.bz2") { + if strings.HasSuffix(filename, ".tar.bz2") { pck, err = conda_module.ParsePackageBZ2(buf) } else { pck, err = conda_module.ParsePackageConda(buf, buf.Size()) diff --git a/routers/api/packages/container/blob.go b/routers/api/packages/container/blob.go index 2ea9b3839c..abfc21f95a 100644 --- a/routers/api/packages/container/blob.go +++ b/routers/api/packages/container/blob.go @@ -90,14 +90,14 @@ func mountBlob(ctx context.Context, pi *packages_service.PackageInfo, pb *packag }) } -func containerPkgName(piOwnerID int64, piName string) string { - return fmt.Sprintf("pkg_%d_container_%s", piOwnerID, strings.ToLower(piName)) +func containerGlobalLockKey(piOwnerID int64, piName, usage string) string { + return fmt.Sprintf("pkg_%d_container_%s_%s", piOwnerID, strings.ToLower(piName), usage) } func getOrCreateUploadVersion(ctx context.Context, pi *packages_service.PackageInfo) (*packages_model.PackageVersion, error) { var uploadVersion *packages_model.PackageVersion - releaser, err := globallock.Lock(ctx, containerPkgName(pi.Owner.ID, pi.Name)) + releaser, err := globallock.Lock(ctx, containerGlobalLockKey(pi.Owner.ID, pi.Name, "package")) if err != nil { return nil, err } @@ -178,7 +178,7 @@ func createFileForBlob(ctx context.Context, pv *packages_model.PackageVersion, p } func deleteBlob(ctx context.Context, ownerID int64, image string, digest digest.Digest) error { - releaser, err := globallock.Lock(ctx, containerPkgName(ownerID, image)) + releaser, err := globallock.Lock(ctx, containerGlobalLockKey(ownerID, image, "blob")) if err != nil { return err } diff --git a/routers/api/packages/container/container.go b/routers/api/packages/container/container.go index d1b80daccf..aeec16be4b 100644 --- a/routers/api/packages/container/container.go +++ b/routers/api/packages/container/container.go @@ -32,7 +32,7 @@ import ( packages_service "code.gitea.io/gitea/services/packages" container_service "code.gitea.io/gitea/services/packages/container" - digest "github.com/opencontainers/go-digest" + "github.com/opencontainers/go-digest" ) // maximum size of a container manifest diff --git a/routers/api/packages/container/manifest.go b/routers/api/packages/container/manifest.go index b69b7af3f7..22ea11c8ce 100644 --- a/routers/api/packages/container/manifest.go +++ b/routers/api/packages/container/manifest.go @@ -16,6 +16,7 @@ import ( packages_model "code.gitea.io/gitea/models/packages" container_model "code.gitea.io/gitea/models/packages/container" user_model "code.gitea.io/gitea/models/user" + "code.gitea.io/gitea/modules/globallock" "code.gitea.io/gitea/modules/json" "code.gitea.io/gitea/modules/log" packages_module "code.gitea.io/gitea/modules/packages" @@ -61,6 +62,13 @@ func processManifest(ctx context.Context, mci *manifestCreationInfo, buf *packag } } + // .../container/manifest.go:453:createManifestBlob() [E] Error inserting package blob: Error 1062 (23000): Duplicate entry '..........' for key 'package_blob.UQE_package_blob_md5' + releaser, err := globallock.Lock(ctx, containerGlobalLockKey(mci.Owner.ID, mci.Image, "manifest")) + if err != nil { + return "", err + } + defer releaser() + if container_module.IsMediaTypeImageManifest(mci.MediaType) { return processOciImageManifest(ctx, mci, buf) } else if container_module.IsMediaTypeImageIndex(mci.MediaType) { diff --git a/routers/api/v1/repo/file.go b/routers/api/v1/repo/file.go index f40d39a251..1c7b57b922 100644 --- a/routers/api/v1/repo/file.go +++ b/routers/api/v1/repo/file.go @@ -470,6 +470,9 @@ func ChangeFiles(ctx *context.APIContext) { ctx.APIError(http.StatusUnprocessableEntity, err) return } + // FIXME: actually now we support more operations like "rename", "upload" + // FIXME: ChangeFileOperation.SHA is NOT required for update or delete if last commit is provided in the options. + // Need to fully fix them in API changeRepoFile := &files_service.ChangeRepoFile{ Operation: file.Operation, TreePath: file.Path, diff --git a/routers/install/install.go b/routers/install/install.go index b9bc41dfcf..c1da79454a 100644 --- a/routers/install/install.go +++ b/routers/install/install.go @@ -601,5 +601,7 @@ func SubmitInstall(ctx *context.Context) { // InstallDone shows the "post-install" page, makes it easier to develop the page. // The name is not called as "PostInstall" to avoid misinterpretation as a handler for "POST /install" func InstallDone(ctx *context.Context) { //nolint + hasUsers, _ := user_model.HasUsers(ctx) + ctx.Data["IsAccountCreated"] = hasUsers.HasAnyUser ctx.HTML(http.StatusOK, tplPostInstall) } diff --git a/routers/web/auth/auth.go b/routers/web/auth/auth.go index 87edbc357b..94f75f69ff 100644 --- a/routers/web/auth/auth.go +++ b/routers/web/auth/auth.go @@ -421,9 +421,11 @@ func SignOut(ctx *context.Context) { // SignUp render the register page func SignUp(ctx *context.Context) { ctx.Data["Title"] = ctx.Tr("sign_up") - ctx.Data["SignUpLink"] = setting.AppSubURL + "/user/sign_up" + hasUsers, _ := user_model.HasUsers(ctx) + ctx.Data["IsFirstTimeRegistration"] = !hasUsers.HasAnyUser + oauth2Providers, err := oauth2.GetOAuth2Providers(ctx, optional.Some(true)) if err != nil { ctx.ServerError("UserSignUp", err) @@ -610,7 +612,13 @@ func createUserInContext(ctx *context.Context, tpl templates.TplName, form any, // sends a confirmation email if required. func handleUserCreated(ctx *context.Context, u *user_model.User, gothUser *goth.User) (ok bool) { // Auto-set admin for the only user. - if user_model.CountUsers(ctx, nil) == 1 { + hasUsers, err := user_model.HasUsers(ctx) + if err != nil { + ctx.ServerError("HasUsers", err) + return false + } + if hasUsers.HasOnlyOneUser { + // the only user is the one just created, will set it as admin opts := &user_service.UpdateOptions{ IsActive: optional.Some(true), IsAdmin: user_service.UpdateOptionFieldFromValue(true), diff --git a/routers/web/explore/repo.go b/routers/web/explore/repo.go index 855c2a50db..f0d7d0ce7d 100644 --- a/routers/web/explore/repo.go +++ b/routers/web/explore/repo.go @@ -151,6 +151,7 @@ func Repos(ctx *context.Context) { ctx.Data["CodePageIsDisabled"] = setting.Service.Explore.DisableCodePage ctx.Data["Title"] = ctx.Tr("explore") ctx.Data["PageIsExplore"] = true + ctx.Data["ShowRepoOwnerOnList"] = true ctx.Data["PageIsExploreRepositories"] = true ctx.Data["IsRepoIndexerEnabled"] = setting.Indexer.RepoIndexerEnabled diff --git a/routers/web/org/setting.go b/routers/web/org/setting.go index 9dd0a98160..2bc1e8bc43 100644 --- a/routers/web/org/setting.go +++ b/routers/web/org/setting.go @@ -18,6 +18,7 @@ import ( repo_module "code.gitea.io/gitea/modules/repository" "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/templates" + "code.gitea.io/gitea/modules/util" "code.gitea.io/gitea/modules/web" shared_user "code.gitea.io/gitea/routers/web/shared/user" user_setting "code.gitea.io/gitea/routers/web/user/setting" @@ -31,8 +32,6 @@ import ( const ( // tplSettingsOptions template path for render settings tplSettingsOptions templates.TplName = "org/settings/options" - // tplSettingsDelete template path for render delete repository - tplSettingsDelete templates.TplName = "org/settings/delete" // tplSettingsHooks template path for render hook settings tplSettingsHooks templates.TplName = "org/settings/hooks" // tplSettingsLabels template path for render labels settings @@ -71,26 +70,6 @@ func SettingsPost(ctx *context.Context) { org := ctx.Org.Organization - if org.Name != form.Name { - if err := user_service.RenameUser(ctx, org.AsUser(), form.Name); err != nil { - if user_model.IsErrUserAlreadyExist(err) { - ctx.Data["Err_Name"] = true - ctx.RenderWithErr(ctx.Tr("form.username_been_taken"), tplSettingsOptions, &form) - } else if db.IsErrNameReserved(err) { - ctx.Data["Err_Name"] = true - ctx.RenderWithErr(ctx.Tr("repo.form.name_reserved", err.(db.ErrNameReserved).Name), tplSettingsOptions, &form) - } else if db.IsErrNamePatternNotAllowed(err) { - ctx.Data["Err_Name"] = true - ctx.RenderWithErr(ctx.Tr("repo.form.name_pattern_not_allowed", err.(db.ErrNamePatternNotAllowed).Pattern), tplSettingsOptions, &form) - } else { - ctx.ServerError("RenameUser", err) - } - return - } - - ctx.Org.OrgLink = setting.AppSubURL + "/org/" + url.PathEscape(org.Name) - } - if form.Email != "" { if err := user_service.ReplacePrimaryEmailAddress(ctx, org.AsUser(), form.Email); err != nil { ctx.Data["Err_Email"] = true @@ -163,42 +142,27 @@ func SettingsDeleteAvatar(ctx *context.Context) { ctx.JSONRedirect(ctx.Org.OrgLink + "/settings") } -// SettingsDelete response for deleting an organization -func SettingsDelete(ctx *context.Context) { - ctx.Data["Title"] = ctx.Tr("org.settings") - ctx.Data["PageIsOrgSettings"] = true - ctx.Data["PageIsSettingsDelete"] = true - - if ctx.Req.Method == http.MethodPost { - if ctx.Org.Organization.Name != ctx.FormString("org_name") { - ctx.Data["Err_OrgName"] = true - ctx.RenderWithErr(ctx.Tr("form.enterred_invalid_org_name"), tplSettingsDelete, nil) - return - } - - if err := org_service.DeleteOrganization(ctx, ctx.Org.Organization, false); err != nil { - if repo_model.IsErrUserOwnRepos(err) { - ctx.Flash.Error(ctx.Tr("form.org_still_own_repo")) - ctx.Redirect(ctx.Org.OrgLink + "/settings/delete") - } else if packages_model.IsErrUserOwnPackages(err) { - ctx.Flash.Error(ctx.Tr("form.org_still_own_packages")) - ctx.Redirect(ctx.Org.OrgLink + "/settings/delete") - } else { - ctx.ServerError("DeleteOrganization", err) - } - } else { - log.Trace("Organization deleted: %s", ctx.Org.Organization.Name) - ctx.Redirect(setting.AppSubURL + "/") - } +// SettingsDeleteOrgPost response for deleting an organization +func SettingsDeleteOrgPost(ctx *context.Context) { + if ctx.Org.Organization.Name != ctx.FormString("org_name") { + ctx.JSONError(ctx.Tr("form.enterred_invalid_org_name")) return } - if _, err := shared_user.RenderUserOrgHeader(ctx); err != nil { - ctx.ServerError("RenderUserOrgHeader", err) + if err := org_service.DeleteOrganization(ctx, ctx.Org.Organization, false /* no purge */); err != nil { + if repo_model.IsErrUserOwnRepos(err) { + ctx.JSONError(ctx.Tr("form.org_still_own_repo")) + } else if packages_model.IsErrUserOwnPackages(err) { + ctx.JSONError(ctx.Tr("form.org_still_own_packages")) + } else { + log.Error("DeleteOrganization: %v", err) + ctx.JSONError(util.Iif(ctx.Doer.IsAdmin, err.Error(), string(ctx.Tr("org.settings.delete_failed")))) + } return } - ctx.HTML(http.StatusOK, tplSettingsDelete) + ctx.Flash.Success(ctx.Tr("org.settings.delete_successful", ctx.Org.Organization.Name)) + ctx.JSONRedirect(setting.AppSubURL + "/") } // Webhooks render webhook list page @@ -250,3 +214,40 @@ func Labels(ctx *context.Context) { ctx.HTML(http.StatusOK, tplSettingsLabels) } + +// SettingsRenamePost response for renaming organization +func SettingsRenamePost(ctx *context.Context) { + form := web.GetForm(ctx).(*forms.RenameOrgForm) + if ctx.HasError() { + ctx.JSONError(ctx.GetErrMsg()) + return + } + + oldOrgName, newOrgName := ctx.Org.Organization.Name, form.NewOrgName + + if form.OrgName != oldOrgName { + ctx.JSONError(ctx.Tr("form.enterred_invalid_org_name")) + return + } + if newOrgName == oldOrgName { + ctx.JSONError(ctx.Tr("org.settings.rename_no_change")) + return + } + + if err := user_service.RenameUser(ctx, ctx.Org.Organization.AsUser(), newOrgName); err != nil { + if user_model.IsErrUserAlreadyExist(err) { + ctx.JSONError(ctx.Tr("org.form.name_been_taken", newOrgName)) + } else if db.IsErrNameReserved(err) { + ctx.JSONError(ctx.Tr("org.form.name_reserved", newOrgName)) + } else if db.IsErrNamePatternNotAllowed(err) { + ctx.JSONError(ctx.Tr("org.form.name_pattern_not_allowed", newOrgName)) + } else { + log.Error("RenameOrganization: %v", err) + ctx.JSONError(util.Iif(ctx.Doer.IsAdmin, err.Error(), string(ctx.Tr("org.settings.rename_failed")))) + } + return + } + + ctx.Flash.Success(ctx.Tr("org.settings.rename_success", oldOrgName, newOrgName)) + ctx.JSONRedirect(setting.AppSubURL + "/org/" + url.PathEscape(newOrgName) + "/settings") +} diff --git a/routers/web/repo/actions/actions.go b/routers/web/repo/actions/actions.go index 7f219811bd..202da407d2 100644 --- a/routers/web/repo/actions/actions.go +++ b/routers/web/repo/actions/actions.go @@ -318,7 +318,7 @@ func prepareWorkflowList(ctx *context.Context, workflows []Workflow) { ctx.Data["Page"] = pager ctx.Data["HasWorkflowsOrRuns"] = len(workflows) > 0 || len(runs) > 0 - ctx.Data["AllowDeleteWorkflowRuns"] = ctx.Repo.CanWrite(unit.TypeActions) + ctx.Data["CanWriteRepoUnitActions"] = ctx.Repo.CanWrite(unit.TypeActions) } // loadIsRefDeleted loads the IsRefDeleted field for each run in the list. diff --git a/routers/web/repo/cherry_pick.go b/routers/web/repo/cherry_pick.go deleted file mode 100644 index 690b830bc2..0000000000 --- a/routers/web/repo/cherry_pick.go +++ /dev/null @@ -1,193 +0,0 @@ -// Copyright 2021 The Gitea Authors. All rights reserved. -// SPDX-License-Identifier: MIT - -package repo - -import ( - "bytes" - "errors" - "net/http" - "strings" - - git_model "code.gitea.io/gitea/models/git" - "code.gitea.io/gitea/models/unit" - "code.gitea.io/gitea/modules/git" - "code.gitea.io/gitea/modules/setting" - "code.gitea.io/gitea/modules/templates" - "code.gitea.io/gitea/modules/util" - "code.gitea.io/gitea/modules/web" - "code.gitea.io/gitea/services/context" - "code.gitea.io/gitea/services/forms" - "code.gitea.io/gitea/services/repository/files" -) - -var tplCherryPick templates.TplName = "repo/editor/cherry_pick" - -// CherryPick handles cherrypick GETs -func CherryPick(ctx *context.Context) { - ctx.Data["SHA"] = ctx.PathParam("sha") - cherryPickCommit, err := ctx.Repo.GitRepo.GetCommit(ctx.PathParam("sha")) - if err != nil { - if git.IsErrNotExist(err) { - ctx.NotFound(err) - return - } - ctx.ServerError("GetCommit", err) - return - } - - if ctx.FormString("cherry-pick-type") == "revert" { - ctx.Data["CherryPickType"] = "revert" - ctx.Data["commit_summary"] = "revert " + ctx.PathParam("sha") - ctx.Data["commit_message"] = "revert " + cherryPickCommit.Message() - } else { - ctx.Data["CherryPickType"] = "cherry-pick" - splits := strings.SplitN(cherryPickCommit.Message(), "\n", 2) - ctx.Data["commit_summary"] = splits[0] - ctx.Data["commit_message"] = splits[1] - } - - canCommit := renderCommitRights(ctx) - ctx.Data["TreePath"] = "" - - if canCommit { - ctx.Data["commit_choice"] = frmCommitChoiceDirect - } else { - ctx.Data["commit_choice"] = frmCommitChoiceNewBranch - } - ctx.Data["new_branch_name"] = GetUniquePatchBranchName(ctx) - ctx.Data["last_commit"] = ctx.Repo.CommitID - ctx.Data["LineWrapExtensions"] = strings.Join(setting.Repository.Editor.LineWrapExtensions, ",") - ctx.Data["BranchLink"] = ctx.Repo.RepoLink + "/src/" + ctx.Repo.RefTypeNameSubURL() - - ctx.HTML(http.StatusOK, tplCherryPick) -} - -// CherryPickPost handles cherrypick POSTs -func CherryPickPost(ctx *context.Context) { - form := web.GetForm(ctx).(*forms.CherryPickForm) - - sha := ctx.PathParam("sha") - ctx.Data["SHA"] = sha - if form.Revert { - ctx.Data["CherryPickType"] = "revert" - } else { - ctx.Data["CherryPickType"] = "cherry-pick" - } - - canCommit := renderCommitRights(ctx) - branchName := ctx.Repo.BranchName - if form.CommitChoice == frmCommitChoiceNewBranch { - branchName = form.NewBranchName - } - ctx.Data["commit_summary"] = form.CommitSummary - ctx.Data["commit_message"] = form.CommitMessage - ctx.Data["commit_choice"] = form.CommitChoice - ctx.Data["new_branch_name"] = form.NewBranchName - ctx.Data["last_commit"] = ctx.Repo.CommitID - ctx.Data["LineWrapExtensions"] = strings.Join(setting.Repository.Editor.LineWrapExtensions, ",") - ctx.Data["BranchLink"] = ctx.Repo.RepoLink + "/src/" + ctx.Repo.RefTypeNameSubURL() - - if ctx.HasError() { - ctx.HTML(http.StatusOK, tplCherryPick) - return - } - - // Cannot commit to a an existing branch if user doesn't have rights - if branchName == ctx.Repo.BranchName && !canCommit { - ctx.Data["Err_NewBranchName"] = true - ctx.Data["commit_choice"] = frmCommitChoiceNewBranch - ctx.RenderWithErr(ctx.Tr("repo.editor.cannot_commit_to_protected_branch", branchName), tplCherryPick, &form) - return - } - - message := strings.TrimSpace(form.CommitSummary) - if message == "" { - if form.Revert { - message = ctx.Locale.TrString("repo.commit.revert-header", sha) - } else { - message = ctx.Locale.TrString("repo.commit.cherry-pick-header", sha) - } - } - - form.CommitMessage = strings.TrimSpace(form.CommitMessage) - if len(form.CommitMessage) > 0 { - message += "\n\n" + form.CommitMessage - } - - gitCommitter, valid := WebGitOperationGetCommitChosenEmailIdentity(ctx, form.CommitEmail) - if !valid { - ctx.Data["Err_CommitEmail"] = true - ctx.RenderWithErr(ctx.Tr("repo.editor.invalid_commit_email"), tplCherryPick, &form) - return - } - opts := &files.ApplyDiffPatchOptions{ - LastCommitID: form.LastCommit, - OldBranch: ctx.Repo.BranchName, - NewBranch: branchName, - Message: message, - Author: gitCommitter, - Committer: gitCommitter, - } - - // First lets try the simple plain read-tree -m approach - opts.Content = sha - if _, err := files.CherryPick(ctx, ctx.Repo.Repository, ctx.Doer, form.Revert, opts); err != nil { - if git_model.IsErrBranchAlreadyExists(err) { - // User has specified a branch that already exists - branchErr := err.(git_model.ErrBranchAlreadyExists) - ctx.Data["Err_NewBranchName"] = true - ctx.RenderWithErr(ctx.Tr("repo.editor.branch_already_exists", branchErr.BranchName), tplCherryPick, &form) - return - } else if files.IsErrCommitIDDoesNotMatch(err) { - ctx.RenderWithErr(ctx.Tr("repo.editor.file_changed_while_editing", ctx.Repo.RepoLink+"/compare/"+form.LastCommit+"..."+ctx.Repo.CommitID), tplPatchFile, &form) - return - } - // Drop through to the apply technique - - buf := &bytes.Buffer{} - if form.Revert { - if err := git.GetReverseRawDiff(ctx, ctx.Repo.Repository.RepoPath(), sha, buf); err != nil { - if git.IsErrNotExist(err) { - ctx.NotFound(errors.New("commit " + ctx.PathParam("sha") + " does not exist.")) - return - } - ctx.ServerError("GetRawDiff", err) - return - } - } else { - if err := git.GetRawDiff(ctx.Repo.GitRepo, sha, git.RawDiffType("patch"), buf); err != nil { - if git.IsErrNotExist(err) { - ctx.NotFound(errors.New("commit " + ctx.PathParam("sha") + " does not exist.")) - return - } - ctx.ServerError("GetRawDiff", err) - return - } - } - - opts.Content = buf.String() - ctx.Data["FileContent"] = opts.Content - - if _, err := files.ApplyDiffPatch(ctx, ctx.Repo.Repository, ctx.Doer, opts); err != nil { - if git_model.IsErrBranchAlreadyExists(err) { - // User has specified a branch that already exists - branchErr := err.(git_model.ErrBranchAlreadyExists) - ctx.Data["Err_NewBranchName"] = true - ctx.RenderWithErr(ctx.Tr("repo.editor.branch_already_exists", branchErr.BranchName), tplCherryPick, &form) - return - } else if files.IsErrCommitIDDoesNotMatch(err) { - ctx.RenderWithErr(ctx.Tr("repo.editor.file_changed_while_editing", ctx.Repo.RepoLink+"/compare/"+form.LastCommit+"..."+ctx.Repo.CommitID), tplPatchFile, &form) - return - } - ctx.RenderWithErr(ctx.Tr("repo.editor.fail_to_apply_patch", err), tplPatchFile, &form) - return - } - } - - if form.CommitChoice == frmCommitChoiceNewBranch && ctx.Repo.Repository.UnitEnabled(ctx, unit.TypePullRequests) { - ctx.Redirect(ctx.Repo.RepoLink + "/compare/" + util.PathEscapeSegments(ctx.Repo.BranchName) + "..." + util.PathEscapeSegments(form.NewBranchName)) - } else { - ctx.Redirect(ctx.Repo.RepoLink + "/src/branch/" + util.PathEscapeSegments(branchName)) - } -} diff --git a/routers/web/repo/editor.go b/routers/web/repo/editor.go index 1a090c9437..ae0b74b019 100644 --- a/routers/web/repo/editor.go +++ b/routers/web/repo/editor.go @@ -4,6 +4,7 @@ package repo import ( + "bytes" "fmt" "io" "net/http" @@ -11,18 +12,16 @@ import ( "strings" git_model "code.gitea.io/gitea/models/git" - repo_model "code.gitea.io/gitea/models/repo" "code.gitea.io/gitea/models/unit" "code.gitea.io/gitea/modules/charset" "code.gitea.io/gitea/modules/git" - "code.gitea.io/gitea/modules/json" + "code.gitea.io/gitea/modules/httplib" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/markup" "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/templates" "code.gitea.io/gitea/modules/util" "code.gitea.io/gitea/modules/web" - "code.gitea.io/gitea/routers/utils" "code.gitea.io/gitea/services/context" "code.gitea.io/gitea/services/context/upload" "code.gitea.io/gitea/services/forms" @@ -34,141 +33,261 @@ const ( tplEditDiffPreview templates.TplName = "repo/editor/diff_preview" tplDeleteFile templates.TplName = "repo/editor/delete" tplUploadFile templates.TplName = "repo/editor/upload" + tplPatchFile templates.TplName = "repo/editor/patch" + tplCherryPick templates.TplName = "repo/editor/cherry_pick" - frmCommitChoiceDirect string = "direct" - frmCommitChoiceNewBranch string = "commit-to-new-branch" + editorCommitChoiceDirect string = "direct" + editorCommitChoiceNewBranch string = "commit-to-new-branch" ) -func canCreateBasePullRequest(ctx *context.Context) bool { - baseRepo := ctx.Repo.Repository.BaseRepo - return baseRepo != nil && baseRepo.UnitEnabled(ctx, unit.TypePullRequests) +func prepareEditorCommitFormOptions(ctx *context.Context, editorAction string) *context.CommitFormOptions { + cleanedTreePath := files_service.CleanGitTreePath(ctx.Repo.TreePath) + if cleanedTreePath != ctx.Repo.TreePath { + redirectTo := fmt.Sprintf("%s/%s/%s/%s", ctx.Repo.RepoLink, editorAction, util.PathEscapeSegments(ctx.Repo.BranchName), util.PathEscapeSegments(cleanedTreePath)) + if ctx.Req.URL.RawQuery != "" { + redirectTo += "?" + ctx.Req.URL.RawQuery + } + ctx.Redirect(redirectTo) + return nil + } + + commitFormOptions, err := context.PrepareCommitFormOptions(ctx, ctx.Doer, ctx.Repo.Repository, ctx.Repo.Permission, ctx.Repo.RefFullName) + if err != nil { + ctx.ServerError("PrepareCommitFormOptions", err) + return nil + } + + if commitFormOptions.NeedFork { + ForkToEdit(ctx) + return nil + } + + if commitFormOptions.WillSubmitToFork && !commitFormOptions.TargetRepo.CanEnableEditor() { + ctx.Data["NotFoundPrompt"] = ctx.Locale.Tr("repo.editor.fork_not_editable") + ctx.NotFound(nil) + } + + ctx.Data["BranchLink"] = ctx.Repo.RepoLink + "/src/" + ctx.Repo.RefTypeNameSubURL() + ctx.Data["TreePath"] = ctx.Repo.TreePath + ctx.Data["CommitFormOptions"] = commitFormOptions + + // for online editor + ctx.Data["PreviewableExtensions"] = strings.Join(markup.PreviewableExtensions(), ",") + ctx.Data["LineWrapExtensions"] = strings.Join(setting.Repository.Editor.LineWrapExtensions, ",") + ctx.Data["IsEditingFileOnly"] = ctx.FormString("return_uri") != "" + ctx.Data["ReturnURI"] = ctx.FormString("return_uri") + + // form fields + ctx.Data["commit_summary"] = "" + ctx.Data["commit_message"] = "" + ctx.Data["commit_choice"] = util.Iif(commitFormOptions.CanCommitToBranch, editorCommitChoiceDirect, editorCommitChoiceNewBranch) + ctx.Data["new_branch_name"] = getUniquePatchBranchName(ctx, ctx.Doer.LowerName, commitFormOptions.TargetRepo) + ctx.Data["last_commit"] = ctx.Repo.CommitID + return commitFormOptions +} + +func prepareTreePathFieldsAndPaths(ctx *context.Context, treePath string) { + // show the tree path fields in the "breadcrumb" and help users to edit the target tree path + ctx.Data["TreeNames"], ctx.Data["TreePaths"] = getParentTreeFields(strings.TrimPrefix(treePath, "/")) +} + +type preparedEditorCommitForm[T any] struct { + form T + commonForm *forms.CommitCommonForm + CommitFormOptions *context.CommitFormOptions + OldBranchName string + NewBranchName string + GitCommitter *files_service.IdentityOptions } -func renderCommitRights(ctx *context.Context) bool { - canCommitToBranch, err := ctx.Repo.CanCommitToBranch(ctx, ctx.Doer) +func (f *preparedEditorCommitForm[T]) GetCommitMessage(defaultCommitMessage string) string { + commitMessage := util.IfZero(strings.TrimSpace(f.commonForm.CommitSummary), defaultCommitMessage) + if body := strings.TrimSpace(f.commonForm.CommitMessage); body != "" { + commitMessage += "\n\n" + body + } + return commitMessage +} + +func prepareEditorCommitSubmittedForm[T forms.CommitCommonFormInterface](ctx *context.Context) *preparedEditorCommitForm[T] { + form := web.GetForm(ctx).(T) + if ctx.HasError() { + ctx.JSONError(ctx.GetErrMsg()) + return nil + } + + commonForm := form.GetCommitCommonForm() + commonForm.TreePath = files_service.CleanGitTreePath(commonForm.TreePath) + + commitFormOptions, err := context.PrepareCommitFormOptions(ctx, ctx.Doer, ctx.Repo.Repository, ctx.Repo.Permission, ctx.Repo.RefFullName) if err != nil { - log.Error("CanCommitToBranch: %v", err) + ctx.ServerError("PrepareCommitFormOptions", err) + return nil + } + if commitFormOptions.NeedFork { + // It shouldn't happen, because we should have done the checks in the "GET" request. But just in case. + ctx.JSONError(ctx.Locale.TrString("error.not_found")) + return nil + } + + // check commit behavior + fromBaseBranch := ctx.FormString("from_base_branch") + commitToNewBranch := commonForm.CommitChoice == editorCommitChoiceNewBranch || fromBaseBranch != "" + targetBranchName := util.Iif(commitToNewBranch, commonForm.NewBranchName, ctx.Repo.BranchName) + if targetBranchName == ctx.Repo.BranchName && !commitFormOptions.CanCommitToBranch { + ctx.JSONError(ctx.Tr("repo.editor.cannot_commit_to_protected_branch", targetBranchName)) + return nil } - ctx.Data["CanCommitToBranch"] = canCommitToBranch - ctx.Data["CanCreatePullRequest"] = ctx.Repo.Repository.UnitEnabled(ctx, unit.TypePullRequests) || canCreateBasePullRequest(ctx) - return canCommitToBranch.CanCommitToBranch + // Committer user info + gitCommitter, valid := WebGitOperationGetCommitChosenEmailIdentity(ctx, commonForm.CommitEmail) + if !valid { + ctx.JSONError(ctx.Tr("repo.editor.invalid_commit_email")) + return nil + } + + if commitToNewBranch { + // if target branch exists, we should stop + targetBranchExists, err := git_model.IsBranchExist(ctx, commitFormOptions.TargetRepo.ID, targetBranchName) + if err != nil { + ctx.ServerError("IsBranchExist", err) + return nil + } else if targetBranchExists { + if fromBaseBranch != "" { + ctx.JSONError(ctx.Tr("repo.editor.fork_branch_exists", targetBranchName)) + } else { + ctx.JSONError(ctx.Tr("repo.editor.branch_already_exists", targetBranchName)) + } + return nil + } + } + + oldBranchName := ctx.Repo.BranchName + if fromBaseBranch != "" { + err = editorPushBranchToForkedRepository(ctx, ctx.Doer, ctx.Repo.Repository.BaseRepo, fromBaseBranch, commitFormOptions.TargetRepo, targetBranchName) + if err != nil { + log.Error("Unable to editorPushBranchToForkedRepository: %v", err) + ctx.JSONError(ctx.Tr("repo.editor.fork_failed_to_push_branch", targetBranchName)) + return nil + } + // we have pushed the base branch as the new branch, now we need to commit the changes directly to the new branch + oldBranchName = targetBranchName + } + + return &preparedEditorCommitForm[T]{ + form: form, + commonForm: commonForm, + CommitFormOptions: commitFormOptions, + OldBranchName: oldBranchName, + NewBranchName: targetBranchName, + GitCommitter: gitCommitter, + } } // redirectForCommitChoice redirects after committing the edit to a branch -func redirectForCommitChoice(ctx *context.Context, commitChoice, newBranchName, treePath string) { - if commitChoice == frmCommitChoiceNewBranch { +func redirectForCommitChoice[T any](ctx *context.Context, parsed *preparedEditorCommitForm[T], treePath string) { + // when editing a file in a PR, it should return to the origin location + if returnURI := ctx.FormString("return_uri"); returnURI != "" && httplib.IsCurrentGiteaSiteURL(ctx, returnURI) { + ctx.JSONRedirect(returnURI) + return + } + + if parsed.commonForm.CommitChoice == editorCommitChoiceNewBranch { // Redirect to a pull request when possible redirectToPullRequest := false - repo := ctx.Repo.Repository - baseBranch := ctx.Repo.BranchName - headBranch := newBranchName - if repo.UnitEnabled(ctx, unit.TypePullRequests) { - redirectToPullRequest = true - } else if canCreateBasePullRequest(ctx) { + repo, baseBranch, headBranch := ctx.Repo.Repository, parsed.OldBranchName, parsed.NewBranchName + if ctx.Repo.Repository.IsFork && parsed.CommitFormOptions.CanCreateBasePullRequest { redirectToPullRequest = true baseBranch = repo.BaseRepo.DefaultBranch headBranch = repo.Owner.Name + "/" + repo.Name + ":" + headBranch repo = repo.BaseRepo + } else if repo.UnitEnabled(ctx, unit.TypePullRequests) { + redirectToPullRequest = true } - if redirectToPullRequest { - ctx.Redirect(repo.Link() + "/compare/" + util.PathEscapeSegments(baseBranch) + "..." + util.PathEscapeSegments(headBranch)) + ctx.JSONRedirect(repo.Link() + "/compare/" + util.PathEscapeSegments(baseBranch) + "..." + util.PathEscapeSegments(headBranch)) return } } - returnURI := ctx.FormString("return_uri") - - ctx.RedirectToCurrentSite( - returnURI, - ctx.Repo.RepoLink+"/src/branch/"+util.PathEscapeSegments(newBranchName)+"/"+util.PathEscapeSegments(treePath), - ) + // redirect to the newly updated file + redirectTo := util.URLJoin(ctx.Repo.RepoLink, "src/branch", util.PathEscapeSegments(parsed.NewBranchName), util.PathEscapeSegments(treePath)) + ctx.JSONRedirect(redirectTo) } -// getParentTreeFields returns list of parent tree names and corresponding tree paths -// based on given tree path. -func getParentTreeFields(treePath string) (treeNames, treePaths []string) { - if len(treePath) == 0 { - return treeNames, treePaths +func editFileOpenExisting(ctx *context.Context) (prefetch []byte, dataRc io.ReadCloser, fInfo *fileInfo) { + entry, err := ctx.Repo.Commit.GetTreeEntryByPath(ctx.Repo.TreePath) + if err != nil { + HandleGitError(ctx, "GetTreeEntryByPath", err) + return nil, nil, nil } - treeNames = strings.Split(treePath, "/") - treePaths = make([]string, len(treeNames)) - for i := range treeNames { - treePaths[i] = strings.Join(treeNames[:i+1], "/") + // No way to edit a directory online. + if entry.IsDir() { + ctx.NotFound(nil) + return nil, nil, nil } - return treeNames, treePaths -} -func editFileCommon(ctx *context.Context, isNewFile bool) { - ctx.Data["PageIsEdit"] = true - ctx.Data["IsNewFile"] = isNewFile - ctx.Data["BranchLink"] = ctx.Repo.RepoLink + "/src/" + ctx.Repo.RefTypeNameSubURL() - ctx.Data["PreviewableExtensions"] = strings.Join(markup.PreviewableExtensions(), ",") - ctx.Data["LineWrapExtensions"] = strings.Join(setting.Repository.Editor.LineWrapExtensions, ",") - ctx.Data["IsEditingFileOnly"] = ctx.FormString("return_uri") != "" - ctx.Data["ReturnURI"] = ctx.FormString("return_uri") -} - -func editFile(ctx *context.Context, isNewFile bool) { - editFileCommon(ctx, isNewFile) - canCommit := renderCommitRights(ctx) - - treePath := cleanUploadFileName(ctx.Repo.TreePath) - if treePath != ctx.Repo.TreePath { - if isNewFile { - ctx.Redirect(path.Join(ctx.Repo.RepoLink, "_new", util.PathEscapeSegments(ctx.Repo.BranchName), util.PathEscapeSegments(treePath))) + blob := entry.Blob() + buf, dataRc, fInfo, err := getFileReader(ctx, ctx.Repo.Repository.ID, blob) + if err != nil { + if git.IsErrNotExist(err) { + ctx.NotFound(err) } else { - ctx.Redirect(path.Join(ctx.Repo.RepoLink, "_edit", util.PathEscapeSegments(ctx.Repo.BranchName), util.PathEscapeSegments(treePath))) + ctx.ServerError("getFileReader", err) } - return + return nil, nil, nil } - // Check if the filename (and additional path) is specified in the querystring - // (filename is a misnomer, but kept for compatibility with GitHub) - filePath, fileName := path.Split(ctx.Req.URL.Query().Get("filename")) - filePath = strings.Trim(filePath, "/") - treeNames, treePaths := getParentTreeFields(path.Join(ctx.Repo.TreePath, filePath)) - - if !isNewFile { - entry, err := ctx.Repo.Commit.GetTreeEntryByPath(ctx.Repo.TreePath) + if fInfo.isLFSFile { + lfsLock, err := git_model.GetTreePathLock(ctx, ctx.Repo.Repository.ID, ctx.Repo.TreePath) if err != nil { - HandleGitError(ctx, "Repo.Commit.GetTreeEntryByPath", err) - return - } - - // No way to edit a directory online. - if entry.IsDir() { + _ = dataRc.Close() + ctx.ServerError("GetTreePathLock", err) + return nil, nil, nil + } else if lfsLock != nil && lfsLock.OwnerID != ctx.Doer.ID { + _ = dataRc.Close() ctx.NotFound(nil) - return + return nil, nil, nil } + } - blob := entry.Blob() + return buf, dataRc, fInfo +} - buf, dataRc, fInfo, err := getFileReader(ctx, ctx.Repo.Repository.ID, blob) - if err != nil { - if git.IsErrNotExist(err) { - ctx.NotFound(err) - } else { - ctx.ServerError("getFileReader", err) - } - return +func EditFile(ctx *context.Context) { + editorAction := ctx.PathParam("editor_action") + isNewFile := editorAction == "_new" + ctx.Data["IsNewFile"] = isNewFile + + // Check if the filename (and additional path) is specified in the querystring + // (filename is a misnomer, but kept for compatibility with GitHub) + urlQuery := ctx.Req.URL.Query() + queryFilename := urlQuery.Get("filename") + if queryFilename != "" { + newTreePath := path.Join(ctx.Repo.TreePath, queryFilename) + redirectTo := fmt.Sprintf("%s/%s/%s/%s", ctx.Repo.RepoLink, editorAction, util.PathEscapeSegments(ctx.Repo.BranchName), util.PathEscapeSegments(newTreePath)) + urlQuery.Del("filename") + if newQueryParams := urlQuery.Encode(); newQueryParams != "" { + redirectTo += "?" + newQueryParams } + ctx.Redirect(redirectTo) + return + } - defer dataRc.Close() + // on the "New File" page, we should add an empty path field to make end users could input a new name + prepareTreePathFieldsAndPaths(ctx, util.Iif(isNewFile, ctx.Repo.TreePath+"/", ctx.Repo.TreePath)) - if fInfo.isLFSFile { - lfsLock, err := git_model.GetTreePathLock(ctx, ctx.Repo.Repository.ID, ctx.Repo.TreePath) - if err != nil { - ctx.ServerError("GetTreePathLock", err) - return - } - if lfsLock != nil && lfsLock.OwnerID != ctx.Doer.ID { - ctx.NotFound(nil) - return - } + prepareEditorCommitFormOptions(ctx, editorAction) + if ctx.Written() { + return + } + + if !isNewFile { + prefetch, dataRc, fInfo := editFileOpenExisting(ctx) + if ctx.Written() { + return } + defer dataRc.Close() ctx.Data["FileSize"] = fInfo.fileSize @@ -179,740 +298,152 @@ func editFile(ctx *context.Context, isNewFile bool) { ctx.Data["NotEditableReason"] = ctx.Tr("repo.editor.cannot_edit_non_text_files") } else if fInfo.fileSize >= setting.UI.MaxDisplayFileSize { ctx.Data["NotEditableReason"] = ctx.Tr("repo.editor.cannot_edit_too_large_file") - } else { - d, _ := io.ReadAll(dataRc) + } - buf = append(buf, d...) + if ctx.Data["NotEditableReason"] == nil { + buf, err := io.ReadAll(io.MultiReader(bytes.NewReader(prefetch), dataRc)) + if err != nil { + ctx.ServerError("ReadAll", err) + return + } if content, err := charset.ToUTF8(buf, charset.ConvertOpts{KeepBOM: true}); err != nil { - log.Error("ToUTF8: %v", err) ctx.Data["FileContent"] = string(buf) } else { ctx.Data["FileContent"] = content } } - } else { - // Append filename from query, or empty string to allow username the new file. - treeNames = append(treeNames, fileName) } - ctx.Data["TreeNames"] = treeNames - ctx.Data["TreePaths"] = treePaths - ctx.Data["commit_summary"] = "" - ctx.Data["commit_message"] = "" - ctx.Data["commit_choice"] = util.Iif(canCommit, frmCommitChoiceDirect, frmCommitChoiceNewBranch) - ctx.Data["new_branch_name"] = GetUniquePatchBranchName(ctx) - ctx.Data["last_commit"] = ctx.Repo.CommitID - - ctx.Data["EditorconfigJson"] = GetEditorConfig(ctx, treePath) - + ctx.Data["EditorconfigJson"] = getContextRepoEditorConfig(ctx, ctx.Repo.TreePath) ctx.HTML(http.StatusOK, tplEditFile) } -// GetEditorConfig returns a editorconfig JSON string for given treePath or "null" -func GetEditorConfig(ctx *context.Context, treePath string) string { - ec, _, err := ctx.Repo.GetEditorconfig() - if err == nil { - def, err := ec.GetDefinitionForFilename(treePath) - if err == nil { - jsonStr, _ := json.Marshal(def) - return string(jsonStr) - } - } - return "null" -} - -// EditFile render edit file page -func EditFile(ctx *context.Context) { - editFile(ctx, false) -} - -// NewFile render create file page -func NewFile(ctx *context.Context) { - editFile(ctx, true) -} - -func editFilePost(ctx *context.Context, form forms.EditRepoFileForm, isNewFile bool) { - editFileCommon(ctx, isNewFile) - ctx.Data["PageHasPosted"] = true - - canCommit := renderCommitRights(ctx) - treeNames, treePaths := getParentTreeFields(form.TreePath) - branchName := ctx.Repo.BranchName - if form.CommitChoice == frmCommitChoiceNewBranch { - branchName = form.NewBranchName - } - - ctx.Data["TreePath"] = form.TreePath - ctx.Data["TreeNames"] = treeNames - ctx.Data["TreePaths"] = treePaths - ctx.Data["FileContent"] = form.Content - ctx.Data["commit_summary"] = form.CommitSummary - ctx.Data["commit_message"] = form.CommitMessage - ctx.Data["commit_choice"] = form.CommitChoice - ctx.Data["new_branch_name"] = form.NewBranchName - ctx.Data["last_commit"] = ctx.Repo.CommitID - ctx.Data["EditorconfigJson"] = GetEditorConfig(ctx, form.TreePath) - - if ctx.HasError() { - ctx.HTML(http.StatusOK, tplEditFile) - return - } - - // Cannot commit to an existing branch if user doesn't have rights - if branchName == ctx.Repo.BranchName && !canCommit { - ctx.Data["Err_NewBranchName"] = true - ctx.Data["commit_choice"] = frmCommitChoiceNewBranch - ctx.RenderWithErr(ctx.Tr("repo.editor.cannot_commit_to_protected_branch", branchName), tplEditFile, &form) +func EditFilePost(ctx *context.Context) { + editorAction := ctx.PathParam("editor_action") + isNewFile := editorAction == "_new" + parsed := prepareEditorCommitSubmittedForm[*forms.EditRepoFileForm](ctx) + if ctx.Written() { return } - // CommitSummary is optional in the web form, if empty, give it a default message based on add or update - // `message` will be both the summary and message combined - message := strings.TrimSpace(form.CommitSummary) - if len(message) == 0 { - if isNewFile { - message = ctx.Locale.TrString("repo.editor.add", form.TreePath) - } else { - message = ctx.Locale.TrString("repo.editor.update", form.TreePath) - } - } - form.CommitMessage = strings.TrimSpace(form.CommitMessage) - if len(form.CommitMessage) > 0 { - message += "\n\n" + form.CommitMessage - } - - gitCommitter, valid := WebGitOperationGetCommitChosenEmailIdentity(ctx, form.CommitEmail) - if !valid { - ctx.Data["Err_CommitEmail"] = true - ctx.RenderWithErr(ctx.Tr("repo.editor.invalid_commit_email"), tplEditFile, &form) - return - } + defaultCommitMessage := util.Iif(isNewFile, ctx.Locale.TrString("repo.editor.add", parsed.form.TreePath), ctx.Locale.TrString("repo.editor.update", parsed.form.TreePath)) var operation string if isNewFile { operation = "create" - } else if form.Content.Has() { + } else if parsed.form.Content.Has() { // The form content only has data if the file is representable as text, is not too large and not in lfs. operation = "update" - } else if ctx.Repo.TreePath != form.TreePath { + } else if ctx.Repo.TreePath != parsed.form.TreePath { // If it doesn't have data, the only possible operation is a "rename" operation = "rename" } else { // It should never happen, just in case - ctx.Flash.Error(ctx.Tr("error.occurred")) - ctx.HTML(http.StatusOK, tplEditFile) + ctx.JSONError(ctx.Tr("error.occurred")) return } - if _, err := files_service.ChangeRepoFiles(ctx, ctx.Repo.Repository, ctx.Doer, &files_service.ChangeRepoFilesOptions{ - LastCommitID: form.LastCommit, - OldBranch: ctx.Repo.BranchName, - NewBranch: branchName, - Message: message, + _, err := files_service.ChangeRepoFiles(ctx, ctx.Repo.Repository, ctx.Doer, &files_service.ChangeRepoFilesOptions{ + LastCommitID: parsed.form.LastCommit, + OldBranch: parsed.OldBranchName, + NewBranch: parsed.NewBranchName, + Message: parsed.GetCommitMessage(defaultCommitMessage), Files: []*files_service.ChangeRepoFile{ { Operation: operation, FromTreePath: ctx.Repo.TreePath, - TreePath: form.TreePath, - ContentReader: strings.NewReader(strings.ReplaceAll(form.Content.Value(), "\r", "")), + TreePath: parsed.form.TreePath, + ContentReader: strings.NewReader(strings.ReplaceAll(parsed.form.Content.Value(), "\r", "")), }, }, - Signoff: form.Signoff, - Author: gitCommitter, - Committer: gitCommitter, - }); err != nil { - // This is where we handle all the errors thrown by files_service.ChangeRepoFiles - if git.IsErrNotExist(err) { - ctx.RenderWithErr(ctx.Tr("repo.editor.file_editing_no_longer_exists", ctx.Repo.TreePath), tplEditFile, &form) - } else if git_model.IsErrLFSFileLocked(err) { - ctx.Data["Err_TreePath"] = true - ctx.RenderWithErr(ctx.Tr("repo.editor.upload_file_is_locked", err.(git_model.ErrLFSFileLocked).Path, err.(git_model.ErrLFSFileLocked).UserName), tplEditFile, &form) - } else if files_service.IsErrFilenameInvalid(err) { - ctx.Data["Err_TreePath"] = true - ctx.RenderWithErr(ctx.Tr("repo.editor.filename_is_invalid", form.TreePath), tplEditFile, &form) - } else if files_service.IsErrFilePathInvalid(err) { - ctx.Data["Err_TreePath"] = true - if fileErr, ok := err.(files_service.ErrFilePathInvalid); ok { - switch fileErr.Type { - case git.EntryModeSymlink: - ctx.RenderWithErr(ctx.Tr("repo.editor.file_is_a_symlink", fileErr.Path), tplEditFile, &form) - case git.EntryModeTree: - ctx.RenderWithErr(ctx.Tr("repo.editor.filename_is_a_directory", fileErr.Path), tplEditFile, &form) - case git.EntryModeBlob: - ctx.RenderWithErr(ctx.Tr("repo.editor.directory_is_a_file", fileErr.Path), tplEditFile, &form) - default: - ctx.RenderWithErr(ctx.Tr("repo.editor.filename_is_invalid", fileErr.Path), tplEditFile, &form) - } - } else { - ctx.HTTPError(http.StatusInternalServerError, err.Error()) - } - } else if files_service.IsErrRepoFileAlreadyExists(err) { - ctx.Data["Err_TreePath"] = true - ctx.RenderWithErr(ctx.Tr("repo.editor.file_already_exists", form.TreePath), tplEditFile, &form) - } else if git.IsErrBranchNotExist(err) { - // For when a user adds/updates a file to a branch that no longer exists - if branchErr, ok := err.(git.ErrBranchNotExist); ok { - ctx.RenderWithErr(ctx.Tr("repo.editor.branch_does_not_exist", branchErr.Name), tplEditFile, &form) - } else { - ctx.HTTPError(http.StatusInternalServerError, err.Error()) - } - } else if git_model.IsErrBranchAlreadyExists(err) { - // For when a user specifies a new branch that already exists - ctx.Data["Err_NewBranchName"] = true - if branchErr, ok := err.(git_model.ErrBranchAlreadyExists); ok { - ctx.RenderWithErr(ctx.Tr("repo.editor.branch_already_exists", branchErr.BranchName), tplEditFile, &form) - } else { - ctx.HTTPError(http.StatusInternalServerError, err.Error()) - } - } else if files_service.IsErrCommitIDDoesNotMatch(err) { - ctx.RenderWithErr(ctx.Tr("repo.editor.commit_id_not_matching"), tplEditFile, &form) - } else if git.IsErrPushOutOfDate(err) { - ctx.RenderWithErr(ctx.Tr("repo.editor.push_out_of_date"), tplEditFile, &form) - } else if git.IsErrPushRejected(err) { - errPushRej := err.(*git.ErrPushRejected) - if len(errPushRej.Message) == 0 { - ctx.RenderWithErr(ctx.Tr("repo.editor.push_rejected_no_message"), tplEditFile, &form) - } else { - flashError, err := ctx.RenderToHTML(tplAlertDetails, map[string]any{ - "Message": ctx.Tr("repo.editor.push_rejected"), - "Summary": ctx.Tr("repo.editor.push_rejected_summary"), - "Details": utils.SanitizeFlashErrorString(errPushRej.Message), - }) - if err != nil { - ctx.ServerError("editFilePost.HTMLString", err) - return - } - ctx.RenderWithErr(flashError, tplEditFile, &form) - } - } else { - flashError, err := ctx.RenderToHTML(tplAlertDetails, map[string]any{ - "Message": ctx.Tr("repo.editor.fail_to_update_file", form.TreePath), - "Summary": ctx.Tr("repo.editor.fail_to_update_file_summary"), - "Details": utils.SanitizeFlashErrorString(err.Error()), - }) - if err != nil { - ctx.ServerError("editFilePost.HTMLString", err) - return - } - ctx.RenderWithErr(flashError, tplEditFile, &form) - } - } - - redirectForCommitChoice(ctx, form.CommitChoice, branchName, form.TreePath) -} - -// EditFilePost response for editing file -func EditFilePost(ctx *context.Context) { - form := web.GetForm(ctx).(*forms.EditRepoFileForm) - editFilePost(ctx, *form, false) -} - -// NewFilePost response for creating file -func NewFilePost(ctx *context.Context) { - form := web.GetForm(ctx).(*forms.EditRepoFileForm) - editFilePost(ctx, *form, true) -} - -// DiffPreviewPost render preview diff page -func DiffPreviewPost(ctx *context.Context) { - form := web.GetForm(ctx).(*forms.EditPreviewDiffForm) - treePath := cleanUploadFileName(ctx.Repo.TreePath) - if len(treePath) == 0 { - ctx.HTTPError(http.StatusInternalServerError, "file name to diff is invalid") - return - } - - entry, err := ctx.Repo.Commit.GetTreeEntryByPath(treePath) - if err != nil { - ctx.HTTPError(http.StatusInternalServerError, "GetTreeEntryByPath: "+err.Error()) - return - } else if entry.IsDir() { - ctx.HTTPError(http.StatusUnprocessableEntity) - return - } - - diff, err := files_service.GetDiffPreview(ctx, ctx.Repo.Repository, ctx.Repo.BranchName, treePath, form.Content) + Signoff: parsed.form.Signoff, + Author: parsed.GitCommitter, + Committer: parsed.GitCommitter, + }) if err != nil { - ctx.HTTPError(http.StatusInternalServerError, "GetDiffPreview: "+err.Error()) + editorHandleFileOperationError(ctx, parsed.NewBranchName, err) return } - if len(diff.Files) != 0 { - ctx.Data["File"] = diff.Files[0] - } - - ctx.HTML(http.StatusOK, tplEditDiffPreview) + redirectForCommitChoice(ctx, parsed, parsed.form.TreePath) } // DeleteFile render delete file page func DeleteFile(ctx *context.Context) { - ctx.Data["PageIsDelete"] = true - ctx.Data["BranchLink"] = ctx.Repo.RepoLink + "/src/" + ctx.Repo.RefTypeNameSubURL() - treePath := cleanUploadFileName(ctx.Repo.TreePath) - - if treePath != ctx.Repo.TreePath { - ctx.Redirect(path.Join(ctx.Repo.RepoLink, "_delete", util.PathEscapeSegments(ctx.Repo.BranchName), util.PathEscapeSegments(treePath))) + prepareEditorCommitFormOptions(ctx, "_delete") + if ctx.Written() { return } - - ctx.Data["TreePath"] = treePath - canCommit := renderCommitRights(ctx) - - ctx.Data["commit_summary"] = "" - ctx.Data["commit_message"] = "" - ctx.Data["last_commit"] = ctx.Repo.CommitID - if canCommit { - ctx.Data["commit_choice"] = frmCommitChoiceDirect - } else { - ctx.Data["commit_choice"] = frmCommitChoiceNewBranch - } - ctx.Data["new_branch_name"] = GetUniquePatchBranchName(ctx) - + ctx.Data["PageIsDelete"] = true ctx.HTML(http.StatusOK, tplDeleteFile) } // DeleteFilePost response for deleting file func DeleteFilePost(ctx *context.Context) { - form := web.GetForm(ctx).(*forms.DeleteRepoFileForm) - canCommit := renderCommitRights(ctx) - branchName := ctx.Repo.BranchName - if form.CommitChoice == frmCommitChoiceNewBranch { - branchName = form.NewBranchName - } - - ctx.Data["PageIsDelete"] = true - ctx.Data["BranchLink"] = ctx.Repo.RepoLink + "/src/" + ctx.Repo.RefTypeNameSubURL() - ctx.Data["TreePath"] = ctx.Repo.TreePath - ctx.Data["commit_summary"] = form.CommitSummary - ctx.Data["commit_message"] = form.CommitMessage - ctx.Data["commit_choice"] = form.CommitChoice - ctx.Data["new_branch_name"] = form.NewBranchName - ctx.Data["last_commit"] = ctx.Repo.CommitID - - if ctx.HasError() { - ctx.HTML(http.StatusOK, tplDeleteFile) + parsed := prepareEditorCommitSubmittedForm[*forms.DeleteRepoFileForm](ctx) + if ctx.Written() { return } - if branchName == ctx.Repo.BranchName && !canCommit { - ctx.Data["Err_NewBranchName"] = true - ctx.Data["commit_choice"] = frmCommitChoiceNewBranch - ctx.RenderWithErr(ctx.Tr("repo.editor.cannot_commit_to_protected_branch", branchName), tplDeleteFile, &form) - return - } - - message := strings.TrimSpace(form.CommitSummary) - if len(message) == 0 { - message = ctx.Locale.TrString("repo.editor.delete", ctx.Repo.TreePath) - } - form.CommitMessage = strings.TrimSpace(form.CommitMessage) - if len(form.CommitMessage) > 0 { - message += "\n\n" + form.CommitMessage - } - - gitCommitter, valid := WebGitOperationGetCommitChosenEmailIdentity(ctx, form.CommitEmail) - if !valid { - ctx.Data["Err_CommitEmail"] = true - ctx.RenderWithErr(ctx.Tr("repo.editor.invalid_commit_email"), tplDeleteFile, &form) - return - } - - if _, err := files_service.ChangeRepoFiles(ctx, ctx.Repo.Repository, ctx.Doer, &files_service.ChangeRepoFilesOptions{ - LastCommitID: form.LastCommit, - OldBranch: ctx.Repo.BranchName, - NewBranch: branchName, + treePath := ctx.Repo.TreePath + _, err := files_service.ChangeRepoFiles(ctx, ctx.Repo.Repository, ctx.Doer, &files_service.ChangeRepoFilesOptions{ + LastCommitID: parsed.form.LastCommit, + OldBranch: parsed.OldBranchName, + NewBranch: parsed.NewBranchName, Files: []*files_service.ChangeRepoFile{ { Operation: "delete", - TreePath: ctx.Repo.TreePath, + TreePath: treePath, }, }, - Message: message, - Signoff: form.Signoff, - Author: gitCommitter, - Committer: gitCommitter, - }); err != nil { - // This is where we handle all the errors thrown by repofiles.DeleteRepoFile - if git.IsErrNotExist(err) || files_service.IsErrRepoFileDoesNotExist(err) { - ctx.RenderWithErr(ctx.Tr("repo.editor.file_deleting_no_longer_exists", ctx.Repo.TreePath), tplDeleteFile, &form) - } else if files_service.IsErrFilenameInvalid(err) { - ctx.Data["Err_TreePath"] = true - ctx.RenderWithErr(ctx.Tr("repo.editor.filename_is_invalid", ctx.Repo.TreePath), tplDeleteFile, &form) - } else if files_service.IsErrFilePathInvalid(err) { - ctx.Data["Err_TreePath"] = true - if fileErr, ok := err.(files_service.ErrFilePathInvalid); ok { - switch fileErr.Type { - case git.EntryModeSymlink: - ctx.RenderWithErr(ctx.Tr("repo.editor.file_is_a_symlink", fileErr.Path), tplDeleteFile, &form) - case git.EntryModeTree: - ctx.RenderWithErr(ctx.Tr("repo.editor.filename_is_a_directory", fileErr.Path), tplDeleteFile, &form) - case git.EntryModeBlob: - ctx.RenderWithErr(ctx.Tr("repo.editor.directory_is_a_file", fileErr.Path), tplDeleteFile, &form) - default: - ctx.ServerError("DeleteRepoFile", err) - } - } else { - ctx.ServerError("DeleteRepoFile", err) - } - } else if git.IsErrBranchNotExist(err) { - // For when a user deletes a file to a branch that no longer exists - if branchErr, ok := err.(git.ErrBranchNotExist); ok { - ctx.RenderWithErr(ctx.Tr("repo.editor.branch_does_not_exist", branchErr.Name), tplDeleteFile, &form) - } else { - ctx.HTTPError(http.StatusInternalServerError, err.Error()) - } - } else if git_model.IsErrBranchAlreadyExists(err) { - // For when a user specifies a new branch that already exists - if branchErr, ok := err.(git_model.ErrBranchAlreadyExists); ok { - ctx.RenderWithErr(ctx.Tr("repo.editor.branch_already_exists", branchErr.BranchName), tplDeleteFile, &form) - } else { - ctx.HTTPError(http.StatusInternalServerError, err.Error()) - } - } else if files_service.IsErrCommitIDDoesNotMatch(err) || git.IsErrPushOutOfDate(err) { - ctx.RenderWithErr(ctx.Tr("repo.editor.file_changed_while_deleting", ctx.Repo.RepoLink+"/compare/"+util.PathEscapeSegments(form.LastCommit)+"..."+util.PathEscapeSegments(ctx.Repo.CommitID)), tplDeleteFile, &form) - } else if git.IsErrPushRejected(err) { - errPushRej := err.(*git.ErrPushRejected) - if len(errPushRej.Message) == 0 { - ctx.RenderWithErr(ctx.Tr("repo.editor.push_rejected_no_message"), tplDeleteFile, &form) - } else { - flashError, err := ctx.RenderToHTML(tplAlertDetails, map[string]any{ - "Message": ctx.Tr("repo.editor.push_rejected"), - "Summary": ctx.Tr("repo.editor.push_rejected_summary"), - "Details": utils.SanitizeFlashErrorString(errPushRej.Message), - }) - if err != nil { - ctx.ServerError("DeleteFilePost.HTMLString", err) - return - } - ctx.RenderWithErr(flashError, tplDeleteFile, &form) - } - } else { - ctx.ServerError("DeleteRepoFile", err) - } + Message: parsed.GetCommitMessage(ctx.Locale.TrString("repo.editor.delete", treePath)), + Signoff: parsed.form.Signoff, + Author: parsed.GitCommitter, + Committer: parsed.GitCommitter, + }) + if err != nil { + editorHandleFileOperationError(ctx, parsed.NewBranchName, err) return } - ctx.Flash.Success(ctx.Tr("repo.editor.file_delete_success", ctx.Repo.TreePath)) - treePath := path.Dir(ctx.Repo.TreePath) - if treePath == "." { - treePath = "" // the file deleted was in the root, so we return the user to the root directory - } - if len(treePath) > 0 { - // Need to get the latest commit since it changed - commit, err := ctx.Repo.GitRepo.GetBranchCommit(ctx.Repo.BranchName) - if err == nil && commit != nil { - // We have the comment, now find what directory we can return the user to - // (must have entries) - treePath = GetClosestParentWithFiles(treePath, commit) - } else { - treePath = "" // otherwise return them to the root of the repo - } - } - - redirectForCommitChoice(ctx, form.CommitChoice, branchName, treePath) + ctx.Flash.Success(ctx.Tr("repo.editor.file_delete_success", treePath)) + redirectTreePath := getClosestParentWithFiles(ctx.Repo.GitRepo, parsed.NewBranchName, treePath) + redirectForCommitChoice(ctx, parsed, redirectTreePath) } -// UploadFile render upload file page func UploadFile(ctx *context.Context) { ctx.Data["PageIsUpload"] = true - upload.AddUploadContext(ctx, "repo") - canCommit := renderCommitRights(ctx) - treePath := cleanUploadFileName(ctx.Repo.TreePath) - if treePath != ctx.Repo.TreePath { - ctx.Redirect(path.Join(ctx.Repo.RepoLink, "_upload", util.PathEscapeSegments(ctx.Repo.BranchName), util.PathEscapeSegments(treePath))) + prepareTreePathFieldsAndPaths(ctx, ctx.Repo.TreePath) + opts := prepareEditorCommitFormOptions(ctx, "_upload") + if ctx.Written() { return } - ctx.Repo.TreePath = treePath - - treeNames, treePaths := getParentTreeFields(ctx.Repo.TreePath) - if len(treeNames) == 0 { - // We must at least have one element for user to input. - treeNames = []string{""} - } - - ctx.Data["TreeNames"] = treeNames - ctx.Data["TreePaths"] = treePaths - ctx.Data["BranchLink"] = ctx.Repo.RepoLink + "/src/" + ctx.Repo.RefTypeNameSubURL() - ctx.Data["commit_summary"] = "" - ctx.Data["commit_message"] = "" - if canCommit { - ctx.Data["commit_choice"] = frmCommitChoiceDirect - } else { - ctx.Data["commit_choice"] = frmCommitChoiceNewBranch - } - ctx.Data["new_branch_name"] = GetUniquePatchBranchName(ctx) + upload.AddUploadContextForRepo(ctx, opts.TargetRepo) ctx.HTML(http.StatusOK, tplUploadFile) } -// UploadFilePost response for uploading file func UploadFilePost(ctx *context.Context) { - form := web.GetForm(ctx).(*forms.UploadRepoFileForm) - ctx.Data["PageIsUpload"] = true - upload.AddUploadContext(ctx, "repo") - canCommit := renderCommitRights(ctx) - - oldBranchName := ctx.Repo.BranchName - branchName := oldBranchName - - if form.CommitChoice == frmCommitChoiceNewBranch { - branchName = form.NewBranchName - } - - form.TreePath = cleanUploadFileName(form.TreePath) - - treeNames, treePaths := getParentTreeFields(form.TreePath) - if len(treeNames) == 0 { - // We must at least have one element for user to input. - treeNames = []string{""} - } - - ctx.Data["TreePath"] = form.TreePath - ctx.Data["TreeNames"] = treeNames - ctx.Data["TreePaths"] = treePaths - ctx.Data["BranchLink"] = ctx.Repo.RepoLink + "/src/branch/" + util.PathEscapeSegments(branchName) - ctx.Data["commit_summary"] = form.CommitSummary - ctx.Data["commit_message"] = form.CommitMessage - ctx.Data["commit_choice"] = form.CommitChoice - ctx.Data["new_branch_name"] = branchName - - if ctx.HasError() { - ctx.HTML(http.StatusOK, tplUploadFile) - return - } - - if oldBranchName != branchName { - if exist, err := git_model.IsBranchExist(ctx, ctx.Repo.Repository.ID, branchName); err == nil && exist { - ctx.Data["Err_NewBranchName"] = true - ctx.RenderWithErr(ctx.Tr("repo.editor.branch_already_exists", branchName), tplUploadFile, &form) - return - } - } else if !canCommit { - ctx.Data["Err_NewBranchName"] = true - ctx.Data["commit_choice"] = frmCommitChoiceNewBranch - ctx.RenderWithErr(ctx.Tr("repo.editor.cannot_commit_to_protected_branch", branchName), tplUploadFile, &form) - return - } - - if !ctx.Repo.Repository.IsEmpty { - var newTreePath string - for _, part := range treeNames { - newTreePath = path.Join(newTreePath, part) - entry, err := ctx.Repo.Commit.GetTreeEntryByPath(newTreePath) - if err != nil { - if git.IsErrNotExist(err) { - break // Means there is no item with that name, so we're good - } - ctx.ServerError("Repo.Commit.GetTreeEntryByPath", err) - return - } - - // User can only upload files to a directory, the directory name shouldn't be an existing file. - if !entry.IsDir() { - ctx.Data["Err_TreePath"] = true - ctx.RenderWithErr(ctx.Tr("repo.editor.directory_is_a_file", part), tplUploadFile, &form) - return - } - } - } - - message := strings.TrimSpace(form.CommitSummary) - if len(message) == 0 { - dir := form.TreePath - if dir == "" { - dir = "/" - } - message = ctx.Locale.TrString("repo.editor.upload_files_to_dir", dir) - } - - form.CommitMessage = strings.TrimSpace(form.CommitMessage) - if len(form.CommitMessage) > 0 { - message += "\n\n" + form.CommitMessage - } - - gitCommitter, valid := WebGitOperationGetCommitChosenEmailIdentity(ctx, form.CommitEmail) - if !valid { - ctx.Data["Err_CommitEmail"] = true - ctx.RenderWithErr(ctx.Tr("repo.editor.invalid_commit_email"), tplUploadFile, &form) - return - } - - if err := files_service.UploadRepoFiles(ctx, ctx.Repo.Repository, ctx.Doer, &files_service.UploadRepoFileOptions{ - LastCommitID: ctx.Repo.CommitID, - OldBranch: oldBranchName, - NewBranch: branchName, - TreePath: form.TreePath, - Message: message, - Files: form.Files, - Signoff: form.Signoff, - Author: gitCommitter, - Committer: gitCommitter, - }); err != nil { - if git_model.IsErrLFSFileLocked(err) { - ctx.Data["Err_TreePath"] = true - ctx.RenderWithErr(ctx.Tr("repo.editor.upload_file_is_locked", err.(git_model.ErrLFSFileLocked).Path, err.(git_model.ErrLFSFileLocked).UserName), tplUploadFile, &form) - } else if files_service.IsErrFilenameInvalid(err) { - ctx.Data["Err_TreePath"] = true - ctx.RenderWithErr(ctx.Tr("repo.editor.filename_is_invalid", form.TreePath), tplUploadFile, &form) - } else if files_service.IsErrFilePathInvalid(err) { - ctx.Data["Err_TreePath"] = true - fileErr := err.(files_service.ErrFilePathInvalid) - switch fileErr.Type { - case git.EntryModeSymlink: - ctx.RenderWithErr(ctx.Tr("repo.editor.file_is_a_symlink", fileErr.Path), tplUploadFile, &form) - case git.EntryModeTree: - ctx.RenderWithErr(ctx.Tr("repo.editor.filename_is_a_directory", fileErr.Path), tplUploadFile, &form) - case git.EntryModeBlob: - ctx.RenderWithErr(ctx.Tr("repo.editor.directory_is_a_file", fileErr.Path), tplUploadFile, &form) - default: - ctx.HTTPError(http.StatusInternalServerError, err.Error()) - } - } else if files_service.IsErrRepoFileAlreadyExists(err) { - ctx.Data["Err_TreePath"] = true - ctx.RenderWithErr(ctx.Tr("repo.editor.file_already_exists", form.TreePath), tplUploadFile, &form) - } else if git.IsErrBranchNotExist(err) { - branchErr := err.(git.ErrBranchNotExist) - ctx.RenderWithErr(ctx.Tr("repo.editor.branch_does_not_exist", branchErr.Name), tplUploadFile, &form) - } else if git_model.IsErrBranchAlreadyExists(err) { - // For when a user specifies a new branch that already exists - ctx.Data["Err_NewBranchName"] = true - branchErr := err.(git_model.ErrBranchAlreadyExists) - ctx.RenderWithErr(ctx.Tr("repo.editor.branch_already_exists", branchErr.BranchName), tplUploadFile, &form) - } else if git.IsErrPushOutOfDate(err) { - ctx.RenderWithErr(ctx.Tr("repo.editor.file_changed_while_editing", ctx.Repo.RepoLink+"/compare/"+util.PathEscapeSegments(ctx.Repo.CommitID)+"..."+util.PathEscapeSegments(form.NewBranchName)), tplUploadFile, &form) - } else if git.IsErrPushRejected(err) { - errPushRej := err.(*git.ErrPushRejected) - if len(errPushRej.Message) == 0 { - ctx.RenderWithErr(ctx.Tr("repo.editor.push_rejected_no_message"), tplUploadFile, &form) - } else { - flashError, err := ctx.RenderToHTML(tplAlertDetails, map[string]any{ - "Message": ctx.Tr("repo.editor.push_rejected"), - "Summary": ctx.Tr("repo.editor.push_rejected_summary"), - "Details": utils.SanitizeFlashErrorString(errPushRej.Message), - }) - if err != nil { - ctx.ServerError("UploadFilePost.HTMLString", err) - return - } - ctx.RenderWithErr(flashError, tplUploadFile, &form) - } - } else { - // os.ErrNotExist - upload file missing in the intervening time?! - log.Error("Error during upload to repo: %-v to filepath: %s on %s from %s: %v", ctx.Repo.Repository, form.TreePath, oldBranchName, form.NewBranchName, err) - ctx.RenderWithErr(ctx.Tr("repo.editor.unable_to_upload_files", form.TreePath, err), tplUploadFile, &form) - } - return - } - - if ctx.Repo.Repository.IsEmpty { - if isEmpty, err := ctx.Repo.GitRepo.IsEmpty(); err == nil && !isEmpty { - _ = repo_model.UpdateRepositoryColsWithAutoTime(ctx, &repo_model.Repository{ID: ctx.Repo.Repository.ID, IsEmpty: false}, "is_empty") - } - } - - redirectForCommitChoice(ctx, form.CommitChoice, branchName, form.TreePath) -} - -func cleanUploadFileName(name string) string { - // Rebase the filename - name = util.PathJoinRel(name) - // Git disallows any filenames to have a .git directory in them. - for part := range strings.SplitSeq(name, "/") { - if strings.ToLower(part) == ".git" { - return "" - } - } - return name -} - -// UploadFileToServer upload file to server file dir not git -func UploadFileToServer(ctx *context.Context) { - file, header, err := ctx.Req.FormFile("file") - if err != nil { - ctx.HTTPError(http.StatusInternalServerError, fmt.Sprintf("FormFile: %v", err)) - return - } - defer file.Close() - - buf := make([]byte, 1024) - n, _ := util.ReadAtMost(file, buf) - if n > 0 { - buf = buf[:n] - } - - err = upload.Verify(buf, header.Filename, setting.Repository.Upload.AllowedTypes) - if err != nil { - ctx.HTTPError(http.StatusBadRequest, err.Error()) - return - } - - name := cleanUploadFileName(header.Filename) - if len(name) == 0 { - ctx.HTTPError(http.StatusInternalServerError, "Upload file name is invalid") + parsed := prepareEditorCommitSubmittedForm[*forms.UploadRepoFileForm](ctx) + if ctx.Written() { return } - upload, err := repo_model.NewUpload(ctx, name, buf, file) - if err != nil { - ctx.HTTPError(http.StatusInternalServerError, fmt.Sprintf("NewUpload: %v", err)) - return - } - - log.Trace("New file uploaded: %s", upload.UUID) - ctx.JSON(http.StatusOK, map[string]string{ - "uuid": upload.UUID, + defaultCommitMessage := ctx.Locale.TrString("repo.editor.upload_files_to_dir", util.IfZero(parsed.form.TreePath, "/")) + err := files_service.UploadRepoFiles(ctx, ctx.Repo.Repository, ctx.Doer, &files_service.UploadRepoFileOptions{ + LastCommitID: parsed.form.LastCommit, + OldBranch: parsed.OldBranchName, + NewBranch: parsed.NewBranchName, + TreePath: parsed.form.TreePath, + Message: parsed.GetCommitMessage(defaultCommitMessage), + Files: parsed.form.Files, + Signoff: parsed.form.Signoff, + Author: parsed.GitCommitter, + Committer: parsed.GitCommitter, }) -} - -// RemoveUploadFileFromServer remove file from server file dir -func RemoveUploadFileFromServer(ctx *context.Context) { - form := web.GetForm(ctx).(*forms.RemoveUploadFileForm) - if len(form.File) == 0 { - ctx.Status(http.StatusNoContent) - return - } - - if err := repo_model.DeleteUploadByUUID(ctx, form.File); err != nil { - ctx.HTTPError(http.StatusInternalServerError, fmt.Sprintf("DeleteUploadByUUID: %v", err)) + if err != nil { + editorHandleFileOperationError(ctx, parsed.NewBranchName, err) return } - - log.Trace("Upload file removed: %s", form.File) - ctx.Status(http.StatusNoContent) -} - -// GetUniquePatchBranchName Gets a unique branch name for a new patch branch -// It will be in the form of <username>-patch-<num> where <num> is the first branch of this format -// that doesn't already exist. If we exceed 1000 tries or an error is thrown, we just return "" so the user has to -// type in the branch name themselves (will be an empty field) -func GetUniquePatchBranchName(ctx *context.Context) string { - prefix := ctx.Doer.LowerName + "-patch-" - for i := 1; i <= 1000; i++ { - branchName := fmt.Sprintf("%s%d", prefix, i) - if exist, err := git_model.IsBranchExist(ctx, ctx.Repo.Repository.ID, branchName); err != nil { - log.Error("GetUniquePatchBranchName: %v", err) - return "" - } else if !exist { - return branchName - } - } - return "" -} - -// GetClosestParentWithFiles Recursively gets the path of parent in a tree that has files (used when file in a tree is -// deleted). Returns "" for the root if no parents other than the root have files. If the given treePath isn't a -// SubTree or it has no entries, we go up one dir and see if we can return the user to that listing. -func GetClosestParentWithFiles(treePath string, commit *git.Commit) string { - if len(treePath) == 0 || treePath == "." { - return "" - } - // see if the tree has entries - if tree, err := commit.SubTree(treePath); err != nil { - // failed to get tree, going up a dir - return GetClosestParentWithFiles(path.Dir(treePath), commit) - } else if entries, err := tree.ListEntries(); err != nil || len(entries) == 0 { - // no files in this dir, going up a dir - return GetClosestParentWithFiles(path.Dir(treePath), commit) - } - return treePath + redirectForCommitChoice(ctx, parsed, parsed.form.TreePath) } diff --git a/routers/web/repo/editor_apply_patch.go b/routers/web/repo/editor_apply_patch.go new file mode 100644 index 0000000000..bd2811cc5f --- /dev/null +++ b/routers/web/repo/editor_apply_patch.go @@ -0,0 +1,51 @@ +// Copyright 2021 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package repo + +import ( + "net/http" + "strings" + + "code.gitea.io/gitea/modules/util" + "code.gitea.io/gitea/services/context" + "code.gitea.io/gitea/services/forms" + "code.gitea.io/gitea/services/repository/files" +) + +func NewDiffPatch(ctx *context.Context) { + prepareEditorCommitFormOptions(ctx, "_diffpatch") + if ctx.Written() { + return + } + + ctx.Data["PageIsPatch"] = true + ctx.HTML(http.StatusOK, tplPatchFile) +} + +// NewDiffPatchPost response for sending patch page +func NewDiffPatchPost(ctx *context.Context) { + parsed := prepareEditorCommitSubmittedForm[*forms.EditRepoFileForm](ctx) + if ctx.Written() { + return + } + + defaultCommitMessage := ctx.Locale.TrString("repo.editor.patch") + _, err := files.ApplyDiffPatch(ctx, ctx.Repo.Repository, ctx.Doer, &files.ApplyDiffPatchOptions{ + LastCommitID: parsed.form.LastCommit, + OldBranch: parsed.OldBranchName, + NewBranch: parsed.NewBranchName, + Message: parsed.GetCommitMessage(defaultCommitMessage), + Content: strings.ReplaceAll(parsed.form.Content.Value(), "\r\n", "\n"), + Author: parsed.GitCommitter, + Committer: parsed.GitCommitter, + }) + if err != nil { + err = util.ErrorWrapLocale(err, "repo.editor.fail_to_apply_patch") + } + if err != nil { + editorHandleFileOperationError(ctx, parsed.NewBranchName, err) + return + } + redirectForCommitChoice(ctx, parsed, parsed.form.TreePath) +} diff --git a/routers/web/repo/editor_cherry_pick.go b/routers/web/repo/editor_cherry_pick.go new file mode 100644 index 0000000000..10c2741b1c --- /dev/null +++ b/routers/web/repo/editor_cherry_pick.go @@ -0,0 +1,86 @@ +// Copyright 2021 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package repo + +import ( + "bytes" + "net/http" + "strings" + + "code.gitea.io/gitea/modules/git" + "code.gitea.io/gitea/modules/util" + "code.gitea.io/gitea/services/context" + "code.gitea.io/gitea/services/forms" + "code.gitea.io/gitea/services/repository/files" +) + +func CherryPick(ctx *context.Context) { + prepareEditorCommitFormOptions(ctx, "_cherrypick") + if ctx.Written() { + return + } + + fromCommitID := ctx.PathParam("sha") + ctx.Data["FromCommitID"] = fromCommitID + cherryPickCommit, err := ctx.Repo.GitRepo.GetCommit(fromCommitID) + if err != nil { + HandleGitError(ctx, "GetCommit", err) + return + } + + if ctx.FormString("cherry-pick-type") == "revert" { + ctx.Data["CherryPickType"] = "revert" + ctx.Data["commit_summary"] = "revert " + ctx.PathParam("sha") + ctx.Data["commit_message"] = "revert " + cherryPickCommit.Message() + } else { + ctx.Data["CherryPickType"] = "cherry-pick" + splits := strings.SplitN(cherryPickCommit.Message(), "\n", 2) + ctx.Data["commit_summary"] = splits[0] + ctx.Data["commit_message"] = splits[1] + } + + ctx.HTML(http.StatusOK, tplCherryPick) +} + +func CherryPickPost(ctx *context.Context) { + fromCommitID := ctx.PathParam("sha") + parsed := prepareEditorCommitSubmittedForm[*forms.CherryPickForm](ctx) + if ctx.Written() { + return + } + + defaultCommitMessage := util.Iif(parsed.form.Revert, ctx.Locale.TrString("repo.commit.revert-header", fromCommitID), ctx.Locale.TrString("repo.commit.cherry-pick-header", fromCommitID)) + opts := &files.ApplyDiffPatchOptions{ + LastCommitID: parsed.form.LastCommit, + OldBranch: parsed.OldBranchName, + NewBranch: parsed.NewBranchName, + Message: parsed.GetCommitMessage(defaultCommitMessage), + Author: parsed.GitCommitter, + Committer: parsed.GitCommitter, + } + + // First try the simple plain read-tree -m approach + opts.Content = fromCommitID + if _, err := files.CherryPick(ctx, ctx.Repo.Repository, ctx.Doer, parsed.form.Revert, opts); err != nil { + // Drop through to the "apply" method + buf := &bytes.Buffer{} + if parsed.form.Revert { + err = git.GetReverseRawDiff(ctx, ctx.Repo.Repository.RepoPath(), fromCommitID, buf) + } else { + err = git.GetRawDiff(ctx.Repo.GitRepo, fromCommitID, "patch", buf) + } + if err == nil { + opts.Content = buf.String() + _, err = files.ApplyDiffPatch(ctx, ctx.Repo.Repository, ctx.Doer, opts) + if err != nil { + err = util.ErrorWrapLocale(err, "repo.editor.fail_to_apply_patch") + } + } + if err != nil { + editorHandleFileOperationError(ctx, parsed.NewBranchName, err) + return + } + } + redirectForCommitChoice(ctx, parsed, parsed.form.TreePath) +} diff --git a/routers/web/repo/editor_error.go b/routers/web/repo/editor_error.go new file mode 100644 index 0000000000..245226a039 --- /dev/null +++ b/routers/web/repo/editor_error.go @@ -0,0 +1,82 @@ +// Copyright 2025 Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package repo + +import ( + "errors" + + git_model "code.gitea.io/gitea/models/git" + "code.gitea.io/gitea/modules/git" + "code.gitea.io/gitea/modules/log" + "code.gitea.io/gitea/modules/setting" + "code.gitea.io/gitea/modules/util" + "code.gitea.io/gitea/routers/utils" + context_service "code.gitea.io/gitea/services/context" + files_service "code.gitea.io/gitea/services/repository/files" +) + +func errorAs[T error](v error) (e T, ok bool) { + if errors.As(v, &e) { + return e, true + } + return e, false +} + +func editorHandleFileOperationErrorRender(ctx *context_service.Context, message, summary, details string) { + flashError, err := ctx.RenderToHTML(tplAlertDetails, map[string]any{ + "Message": message, + "Summary": summary, + "Details": utils.SanitizeFlashErrorString(details), + }) + if err == nil { + ctx.JSONError(flashError) + } else { + log.Error("RenderToHTML: %v", err) + ctx.JSONError(message + "\n" + summary + "\n" + utils.SanitizeFlashErrorString(details)) + } +} + +func editorHandleFileOperationError(ctx *context_service.Context, targetBranchName string, err error) { + if errAs := util.ErrorAsLocale(err); errAs != nil { + ctx.JSONError(ctx.Tr(errAs.TrKey, errAs.TrArgs...)) + } else if errAs, ok := errorAs[git.ErrNotExist](err); ok { + ctx.JSONError(ctx.Tr("repo.editor.file_modifying_no_longer_exists", errAs.RelPath)) + } else if errAs, ok := errorAs[git_model.ErrLFSFileLocked](err); ok { + ctx.JSONError(ctx.Tr("repo.editor.upload_file_is_locked", errAs.Path, errAs.UserName)) + } else if errAs, ok := errorAs[files_service.ErrFilenameInvalid](err); ok { + ctx.JSONError(ctx.Tr("repo.editor.filename_is_invalid", errAs.Path)) + } else if errAs, ok := errorAs[files_service.ErrFilePathInvalid](err); ok { + switch errAs.Type { + case git.EntryModeSymlink: + ctx.JSONError(ctx.Tr("repo.editor.file_is_a_symlink", errAs.Path)) + case git.EntryModeTree: + ctx.JSONError(ctx.Tr("repo.editor.filename_is_a_directory", errAs.Path)) + case git.EntryModeBlob: + ctx.JSONError(ctx.Tr("repo.editor.directory_is_a_file", errAs.Path)) + default: + ctx.JSONError(ctx.Tr("repo.editor.filename_is_invalid", errAs.Path)) + } + } else if errAs, ok := errorAs[files_service.ErrRepoFileAlreadyExists](err); ok { + ctx.JSONError(ctx.Tr("repo.editor.file_already_exists", errAs.Path)) + } else if errAs, ok := errorAs[git.ErrBranchNotExist](err); ok { + ctx.JSONError(ctx.Tr("repo.editor.branch_does_not_exist", errAs.Name)) + } else if errAs, ok := errorAs[git_model.ErrBranchAlreadyExists](err); ok { + ctx.JSONError(ctx.Tr("repo.editor.branch_already_exists", errAs.BranchName)) + } else if files_service.IsErrCommitIDDoesNotMatch(err) { + ctx.JSONError(ctx.Tr("repo.editor.commit_id_not_matching")) + } else if files_service.IsErrCommitIDDoesNotMatch(err) || git.IsErrPushOutOfDate(err) { + ctx.JSONError(ctx.Tr("repo.editor.file_changed_while_editing", ctx.Repo.RepoLink+"/compare/"+util.PathEscapeSegments(ctx.Repo.CommitID)+"..."+util.PathEscapeSegments(targetBranchName))) + } else if errAs, ok := errorAs[*git.ErrPushRejected](err); ok { + if errAs.Message == "" { + ctx.JSONError(ctx.Tr("repo.editor.push_rejected_no_message")) + } else { + editorHandleFileOperationErrorRender(ctx, ctx.Locale.TrString("repo.editor.push_rejected"), ctx.Locale.TrString("repo.editor.push_rejected_summary"), errAs.Message) + } + } else if errors.Is(err, util.ErrNotExist) { + ctx.JSONError(ctx.Tr("error.not_found")) + } else { + setting.PanicInDevOrTesting("unclear err %T: %v", err, err) + editorHandleFileOperationErrorRender(ctx, ctx.Locale.TrString("repo.editor.failed_to_commit"), ctx.Locale.TrString("repo.editor.failed_to_commit_summary"), err.Error()) + } +} diff --git a/routers/web/repo/editor_fork.go b/routers/web/repo/editor_fork.go new file mode 100644 index 0000000000..b78a634c00 --- /dev/null +++ b/routers/web/repo/editor_fork.go @@ -0,0 +1,31 @@ +// Copyright 2025 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package repo + +import ( + "net/http" + + "code.gitea.io/gitea/modules/templates" + "code.gitea.io/gitea/services/context" + repo_service "code.gitea.io/gitea/services/repository" +) + +const tplEditorFork templates.TplName = "repo/editor/fork" + +func ForkToEdit(ctx *context.Context) { + ctx.HTML(http.StatusOK, tplEditorFork) +} + +func ForkToEditPost(ctx *context.Context) { + ForkRepoTo(ctx, ctx.Doer, repo_service.ForkRepoOptions{ + BaseRepo: ctx.Repo.Repository, + Name: getUniqueRepositoryName(ctx, ctx.Doer.ID, ctx.Repo.Repository.Name), + Description: ctx.Repo.Repository.Description, + SingleBranch: ctx.Repo.Repository.DefaultBranch, // maybe we only need the default branch in the fork? + }) + if ctx.Written() { + return + } + ctx.JSONRedirect("") // reload the page, the new fork should be editable now +} diff --git a/routers/web/repo/editor_preview.go b/routers/web/repo/editor_preview.go new file mode 100644 index 0000000000..14be5b72b6 --- /dev/null +++ b/routers/web/repo/editor_preview.go @@ -0,0 +1,41 @@ +// Copyright 2025 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package repo + +import ( + "net/http" + + "code.gitea.io/gitea/services/context" + files_service "code.gitea.io/gitea/services/repository/files" +) + +func DiffPreviewPost(ctx *context.Context) { + content := ctx.FormString("content") + treePath := files_service.CleanGitTreePath(ctx.Repo.TreePath) + if treePath == "" { + ctx.HTTPError(http.StatusBadRequest, "file name to diff is invalid") + return + } + + entry, err := ctx.Repo.Commit.GetTreeEntryByPath(treePath) + if err != nil { + ctx.ServerError("GetTreeEntryByPath", err) + return + } else if entry.IsDir() { + ctx.HTTPError(http.StatusUnprocessableEntity) + return + } + + diff, err := files_service.GetDiffPreview(ctx, ctx.Repo.Repository, ctx.Repo.BranchName, treePath, content) + if err != nil { + ctx.ServerError("GetDiffPreview", err) + return + } + + if len(diff.Files) != 0 { + ctx.Data["File"] = diff.Files[0] + } + + ctx.HTML(http.StatusOK, tplEditDiffPreview) +} diff --git a/routers/web/repo/editor_test.go b/routers/web/repo/editor_test.go index 89bf8f309c..6e2c1d6219 100644 --- a/routers/web/repo/editor_test.go +++ b/routers/web/repo/editor_test.go @@ -6,76 +6,27 @@ package repo import ( "testing" + repo_model "code.gitea.io/gitea/models/repo" "code.gitea.io/gitea/models/unittest" "code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/gitrepo" - "code.gitea.io/gitea/services/contexttest" "github.com/stretchr/testify/assert" ) -func TestCleanUploadName(t *testing.T) { +func TestEditorUtils(t *testing.T) { unittest.PrepareTestEnv(t) - - kases := map[string]string{ - ".git/refs/master": "", - "/root/abc": "root/abc", - "./../../abc": "abc", - "a/../.git": "", - "a/../../../abc": "abc", - "../../../acd": "acd", - "../../.git/abc": "", - "..\\..\\.git/abc": "..\\..\\.git/abc", - "..\\../.git/abc": "", - "..\\../.git": "", - "abc/../def": "def", - ".drone.yml": ".drone.yml", - ".abc/def/.drone.yml": ".abc/def/.drone.yml", - "..drone.yml.": "..drone.yml.", - "..a.dotty...name...": "..a.dotty...name...", - "..a.dotty../.folder../.name...": "..a.dotty../.folder../.name...", - } - for k, v := range kases { - assert.Equal(t, cleanUploadFileName(k), v) - } -} - -func TestGetUniquePatchBranchName(t *testing.T) { - unittest.PrepareTestEnv(t) - ctx, _ := contexttest.MockContext(t, "user2/repo1") - ctx.SetPathParam("id", "1") - contexttest.LoadRepo(t, ctx, 1) - contexttest.LoadRepoCommit(t, ctx) - contexttest.LoadUser(t, ctx, 2) - contexttest.LoadGitRepo(t, ctx) - defer ctx.Repo.GitRepo.Close() - - expectedBranchName := "user2-patch-1" - branchName := GetUniquePatchBranchName(ctx) - assert.Equal(t, expectedBranchName, branchName) -} - -func TestGetClosestParentWithFiles(t *testing.T) { - unittest.PrepareTestEnv(t) - ctx, _ := contexttest.MockContext(t, "user2/repo1") - ctx.SetPathParam("id", "1") - contexttest.LoadRepo(t, ctx, 1) - contexttest.LoadRepoCommit(t, ctx) - contexttest.LoadUser(t, ctx, 2) - contexttest.LoadGitRepo(t, ctx) - defer ctx.Repo.GitRepo.Close() - - repo := ctx.Repo.Repository - branch := repo.DefaultBranch - gitRepo, _ := gitrepo.OpenRepository(git.DefaultContext, repo) - defer gitRepo.Close() - commit, _ := gitRepo.GetBranchCommit(branch) - var expectedTreePath string // Should return the root dir, empty string, since there are no subdirs in this repo - for _, deletedFile := range []string{ - "dir1/dir2/dir3/file.txt", - "file.txt", - } { - treePath := GetClosestParentWithFiles(deletedFile, commit) - assert.Equal(t, expectedTreePath, treePath) - } + repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1}) + t.Run("getUniquePatchBranchName", func(t *testing.T) { + branchName := getUniquePatchBranchName(t.Context(), "user2", repo) + assert.Equal(t, "user2-patch-1", branchName) + }) + t.Run("getClosestParentWithFiles", func(t *testing.T) { + gitRepo, _ := gitrepo.OpenRepository(git.DefaultContext, repo) + defer gitRepo.Close() + treePath := getClosestParentWithFiles(gitRepo, "sub-home-md-img-check", "docs/foo/bar") + assert.Equal(t, "docs", treePath) + treePath = getClosestParentWithFiles(gitRepo, "sub-home-md-img-check", "any/other") + assert.Empty(t, treePath) + }) } diff --git a/routers/web/repo/editor_uploader.go b/routers/web/repo/editor_uploader.go new file mode 100644 index 0000000000..1ce9a1aca4 --- /dev/null +++ b/routers/web/repo/editor_uploader.go @@ -0,0 +1,61 @@ +// Copyright 2025 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package repo + +import ( + "net/http" + + repo_model "code.gitea.io/gitea/models/repo" + "code.gitea.io/gitea/modules/setting" + "code.gitea.io/gitea/modules/util" + "code.gitea.io/gitea/services/context" + "code.gitea.io/gitea/services/context/upload" + files_service "code.gitea.io/gitea/services/repository/files" +) + +// UploadFileToServer upload file to server file dir not git +func UploadFileToServer(ctx *context.Context) { + file, header, err := ctx.Req.FormFile("file") + if err != nil { + ctx.ServerError("FormFile", err) + return + } + defer file.Close() + + buf := make([]byte, 1024) + n, _ := util.ReadAtMost(file, buf) + if n > 0 { + buf = buf[:n] + } + + err = upload.Verify(buf, header.Filename, setting.Repository.Upload.AllowedTypes) + if err != nil { + ctx.HTTPError(http.StatusBadRequest, err.Error()) + return + } + + name := files_service.CleanGitTreePath(header.Filename) + if len(name) == 0 { + ctx.HTTPError(http.StatusBadRequest, "Upload file name is invalid") + return + } + + uploaded, err := repo_model.NewUpload(ctx, name, buf, file) + if err != nil { + ctx.ServerError("NewUpload", err) + return + } + + ctx.JSON(http.StatusOK, map[string]string{"uuid": uploaded.UUID}) +} + +// RemoveUploadFileFromServer remove file from server file dir +func RemoveUploadFileFromServer(ctx *context.Context) { + fileUUID := ctx.FormString("file") + if err := repo_model.DeleteUploadByUUID(ctx, fileUUID); err != nil { + ctx.ServerError("DeleteUploadByUUID", err) + return + } + ctx.Status(http.StatusNoContent) +} diff --git a/routers/web/repo/editor_util.go b/routers/web/repo/editor_util.go new file mode 100644 index 0000000000..f910f0bd40 --- /dev/null +++ b/routers/web/repo/editor_util.go @@ -0,0 +1,110 @@ +// Copyright 2025 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package repo + +import ( + "context" + "fmt" + "path" + "strings" + + git_model "code.gitea.io/gitea/models/git" + repo_model "code.gitea.io/gitea/models/repo" + user_model "code.gitea.io/gitea/models/user" + "code.gitea.io/gitea/modules/git" + "code.gitea.io/gitea/modules/json" + "code.gitea.io/gitea/modules/log" + repo_module "code.gitea.io/gitea/modules/repository" + context_service "code.gitea.io/gitea/services/context" +) + +// getUniquePatchBranchName Gets a unique branch name for a new patch branch +// It will be in the form of <username>-patch-<num> where <num> is the first branch of this format +// that doesn't already exist. If we exceed 1000 tries or an error is thrown, we just return "" so the user has to +// type in the branch name themselves (will be an empty field) +func getUniquePatchBranchName(ctx context.Context, prefixName string, repo *repo_model.Repository) string { + prefix := prefixName + "-patch-" + for i := 1; i <= 1000; i++ { + branchName := fmt.Sprintf("%s%d", prefix, i) + if exist, err := git_model.IsBranchExist(ctx, repo.ID, branchName); err != nil { + log.Error("getUniquePatchBranchName: %v", err) + return "" + } else if !exist { + return branchName + } + } + return "" +} + +// getClosestParentWithFiles Recursively gets the closest path of parent in a tree that has files when a file in a tree is +// deleted. It returns "" for the tree root if no parents other than the root have files. +func getClosestParentWithFiles(gitRepo *git.Repository, branchName, originTreePath string) string { + var f func(treePath string, commit *git.Commit) string + f = func(treePath string, commit *git.Commit) string { + if treePath == "" || treePath == "." { + return "" + } + // see if the tree has entries + if tree, err := commit.SubTree(treePath); err != nil { + return f(path.Dir(treePath), commit) // failed to get the tree, going up a dir + } else if entries, err := tree.ListEntries(); err != nil || len(entries) == 0 { + return f(path.Dir(treePath), commit) // no files in this dir, going up a dir + } + return treePath + } + commit, err := gitRepo.GetBranchCommit(branchName) // must get the commit again to get the latest change + if err != nil { + log.Error("GetBranchCommit: %v", err) + return "" + } + return f(originTreePath, commit) +} + +// getContextRepoEditorConfig returns the editorconfig JSON string for given treePath or "null" +func getContextRepoEditorConfig(ctx *context_service.Context, treePath string) string { + ec, _, err := ctx.Repo.GetEditorconfig() + if err == nil { + def, err := ec.GetDefinitionForFilename(treePath) + if err == nil { + jsonStr, _ := json.Marshal(def) + return string(jsonStr) + } + } + return "null" +} + +// getParentTreeFields returns list of parent tree names and corresponding tree paths based on given treePath. +// eg: []{"a", "b", "c"}, []{"a", "a/b", "a/b/c"} +// or: []{""}, []{""} for the root treePath +func getParentTreeFields(treePath string) (treeNames, treePaths []string) { + treeNames = strings.Split(treePath, "/") + treePaths = make([]string, len(treeNames)) + for i := range treeNames { + treePaths[i] = strings.Join(treeNames[:i+1], "/") + } + return treeNames, treePaths +} + +// getUniqueRepositoryName Gets a unique repository name for a user +// It will append a -<num> postfix if the name is already taken +func getUniqueRepositoryName(ctx context.Context, ownerID int64, name string) string { + uniqueName := name + for i := 1; i < 1000; i++ { + _, err := repo_model.GetRepositoryByName(ctx, ownerID, uniqueName) + if err != nil || repo_model.IsErrRepoNotExist(err) { + return uniqueName + } + uniqueName = fmt.Sprintf("%s-%d", name, i) + i++ + } + return "" +} + +func editorPushBranchToForkedRepository(ctx context.Context, doer *user_model.User, baseRepo *repo_model.Repository, baseBranchName string, targetRepo *repo_model.Repository, targetBranchName string) error { + return git.Push(ctx, baseRepo.RepoPath(), git.PushOptions{ + Remote: targetRepo.RepoPath(), + Branch: baseBranchName + ":" + targetBranchName, + Env: repo_module.PushingEnvironment(doer, targetRepo), + }) +} diff --git a/routers/web/repo/fork.go b/routers/web/repo/fork.go index 9f5cda10c2..c2694e540f 100644 --- a/routers/web/repo/fork.go +++ b/routers/web/repo/fork.go @@ -189,17 +189,25 @@ func ForkPost(ctx *context.Context) { } } - repo, err := repo_service.ForkRepository(ctx, ctx.Doer, ctxUser, repo_service.ForkRepoOptions{ + repo := ForkRepoTo(ctx, ctxUser, repo_service.ForkRepoOptions{ BaseRepo: forkRepo, Name: form.RepoName, Description: form.Description, SingleBranch: form.ForkSingleBranch, }) + if ctx.Written() { + return + } + ctx.JSONRedirect(ctxUser.HomeLink() + "/" + url.PathEscape(repo.Name)) +} + +func ForkRepoTo(ctx *context.Context, owner *user_model.User, forkOpts repo_service.ForkRepoOptions) *repo_model.Repository { + repo, err := repo_service.ForkRepository(ctx, ctx.Doer, owner, forkOpts) if err != nil { ctx.Data["Err_RepoName"] = true switch { case repo_model.IsErrReachLimitOfRepo(err): - maxCreationLimit := ctxUser.MaxCreationLimit() + maxCreationLimit := owner.MaxCreationLimit() msg := ctx.TrN(maxCreationLimit, "repo.form.reach_limit_of_creation_1", "repo.form.reach_limit_of_creation_n", maxCreationLimit) ctx.JSONError(msg) case repo_model.IsErrRepoAlreadyExist(err): @@ -224,9 +232,7 @@ func ForkPost(ctx *context.Context) { default: ctx.ServerError("ForkPost", err) } - return + return nil } - - log.Trace("Repository forked[%d]: %s/%s", forkRepo.ID, ctxUser.Name, repo.Name) - ctx.JSONRedirect(ctxUser.HomeLink() + "/" + url.PathEscape(repo.Name)) + return repo } diff --git a/routers/web/repo/patch.go b/routers/web/repo/patch.go deleted file mode 100644 index 3ffd8f89c4..0000000000 --- a/routers/web/repo/patch.go +++ /dev/null @@ -1,126 +0,0 @@ -// Copyright 2021 The Gitea Authors. All rights reserved. -// SPDX-License-Identifier: MIT - -package repo - -import ( - "net/http" - "strings" - - git_model "code.gitea.io/gitea/models/git" - "code.gitea.io/gitea/models/unit" - "code.gitea.io/gitea/modules/setting" - "code.gitea.io/gitea/modules/templates" - "code.gitea.io/gitea/modules/util" - "code.gitea.io/gitea/modules/web" - "code.gitea.io/gitea/services/context" - "code.gitea.io/gitea/services/forms" - "code.gitea.io/gitea/services/repository/files" -) - -const ( - tplPatchFile templates.TplName = "repo/editor/patch" -) - -// NewDiffPatch render create patch page -func NewDiffPatch(ctx *context.Context) { - canCommit := renderCommitRights(ctx) - - ctx.Data["PageIsPatch"] = true - - ctx.Data["commit_summary"] = "" - ctx.Data["commit_message"] = "" - if canCommit { - ctx.Data["commit_choice"] = frmCommitChoiceDirect - } else { - ctx.Data["commit_choice"] = frmCommitChoiceNewBranch - } - ctx.Data["new_branch_name"] = GetUniquePatchBranchName(ctx) - ctx.Data["last_commit"] = ctx.Repo.CommitID - ctx.Data["LineWrapExtensions"] = strings.Join(setting.Repository.Editor.LineWrapExtensions, ",") - ctx.Data["BranchLink"] = ctx.Repo.RepoLink + "/src/" + ctx.Repo.RefTypeNameSubURL() - - ctx.HTML(http.StatusOK, tplPatchFile) -} - -// NewDiffPatchPost response for sending patch page -func NewDiffPatchPost(ctx *context.Context) { - form := web.GetForm(ctx).(*forms.EditRepoFileForm) - - canCommit := renderCommitRights(ctx) - branchName := ctx.Repo.BranchName - if form.CommitChoice == frmCommitChoiceNewBranch { - branchName = form.NewBranchName - } - ctx.Data["PageIsPatch"] = true - ctx.Data["BranchLink"] = ctx.Repo.RepoLink + "/src/" + ctx.Repo.RefTypeNameSubURL() - ctx.Data["FileContent"] = form.Content - ctx.Data["commit_summary"] = form.CommitSummary - ctx.Data["commit_message"] = form.CommitMessage - ctx.Data["commit_choice"] = form.CommitChoice - ctx.Data["new_branch_name"] = form.NewBranchName - ctx.Data["last_commit"] = ctx.Repo.CommitID - ctx.Data["LineWrapExtensions"] = strings.Join(setting.Repository.Editor.LineWrapExtensions, ",") - - if ctx.HasError() { - ctx.HTML(http.StatusOK, tplPatchFile) - return - } - - // Cannot commit to an existing branch if user doesn't have rights - if branchName == ctx.Repo.BranchName && !canCommit { - ctx.Data["Err_NewBranchName"] = true - ctx.Data["commit_choice"] = frmCommitChoiceNewBranch - ctx.RenderWithErr(ctx.Tr("repo.editor.cannot_commit_to_protected_branch", branchName), tplEditFile, &form) - return - } - - // CommitSummary is optional in the web form, if empty, give it a default message based on add or update - // `message` will be both the summary and message combined - message := strings.TrimSpace(form.CommitSummary) - if len(message) == 0 { - message = ctx.Locale.TrString("repo.editor.patch") - } - - form.CommitMessage = strings.TrimSpace(form.CommitMessage) - if len(form.CommitMessage) > 0 { - message += "\n\n" + form.CommitMessage - } - - gitCommitter, valid := WebGitOperationGetCommitChosenEmailIdentity(ctx, form.CommitEmail) - if !valid { - ctx.Data["Err_CommitEmail"] = true - ctx.RenderWithErr(ctx.Tr("repo.editor.invalid_commit_email"), tplPatchFile, &form) - return - } - - fileResponse, err := files.ApplyDiffPatch(ctx, ctx.Repo.Repository, ctx.Doer, &files.ApplyDiffPatchOptions{ - LastCommitID: form.LastCommit, - OldBranch: ctx.Repo.BranchName, - NewBranch: branchName, - Message: message, - Content: strings.ReplaceAll(form.Content.Value(), "\r", ""), - Author: gitCommitter, - Committer: gitCommitter, - }) - if err != nil { - if git_model.IsErrBranchAlreadyExists(err) { - // User has specified a branch that already exists - branchErr := err.(git_model.ErrBranchAlreadyExists) - ctx.Data["Err_NewBranchName"] = true - ctx.RenderWithErr(ctx.Tr("repo.editor.branch_already_exists", branchErr.BranchName), tplEditFile, &form) - return - } else if files.IsErrCommitIDDoesNotMatch(err) { - ctx.RenderWithErr(ctx.Tr("repo.editor.file_changed_while_editing", ctx.Repo.RepoLink+"/compare/"+form.LastCommit+"..."+ctx.Repo.CommitID), tplPatchFile, &form) - return - } - ctx.RenderWithErr(ctx.Tr("repo.editor.fail_to_apply_patch", err), tplPatchFile, &form) - return - } - - if form.CommitChoice == frmCommitChoiceNewBranch && ctx.Repo.Repository.UnitEnabled(ctx, unit.TypePullRequests) { - ctx.Redirect(ctx.Repo.RepoLink + "/compare/" + util.PathEscapeSegments(ctx.Repo.BranchName) + "..." + util.PathEscapeSegments(form.NewBranchName)) - } else { - ctx.Redirect(ctx.Repo.RepoLink + "/commit/" + fileResponse.Commit.SHA) - } -} diff --git a/routers/web/repo/view.go b/routers/web/repo/view.go index 68fa17bf07..f0d90f9533 100644 --- a/routers/web/repo/view.go +++ b/routers/web/repo/view.go @@ -394,9 +394,10 @@ func Forks(ctx *context.Context) { } pager := context.NewPagination(int(total), pageSize, page, 5) + ctx.Data["ShowRepoOwnerAvatar"] = true + ctx.Data["ShowRepoOwnerOnList"] = true ctx.Data["Page"] = pager - - ctx.Data["Forks"] = forks + ctx.Data["Repos"] = forks ctx.HTML(http.StatusOK, tplForks) } diff --git a/routers/web/repo/view_file.go b/routers/web/repo/view_file.go index ec0ad02828..5606a8e6ec 100644 --- a/routers/web/repo/view_file.go +++ b/routers/web/repo/view_file.go @@ -290,7 +290,7 @@ func prepareToRenderFile(ctx *context.Context, entry *git.TreeEntry) { func prepareToRenderButtons(ctx *context.Context, lfsLock *git_model.LFSLock) { // archived or mirror repository, the buttons should not be shown - if ctx.Repo.Repository.IsArchived || !ctx.Repo.Repository.CanEnableEditor() { + if !ctx.Repo.Repository.CanEnableEditor() { return } @@ -302,7 +302,9 @@ func prepareToRenderButtons(ctx *context.Context, lfsLock *git_model.LFSLock) { } if !ctx.Repo.CanWriteToBranch(ctx, ctx.Doer, ctx.Repo.BranchName) { + ctx.Data["CanEditFile"] = true ctx.Data["EditFileTooltip"] = ctx.Tr("repo.editor.fork_before_edit") + ctx.Data["CanDeleteFile"] = true ctx.Data["DeleteFileTooltip"] = ctx.Tr("repo.editor.must_have_write_access") return } diff --git a/routers/web/repo/view_readme.go b/routers/web/repo/view_readme.go index 7af6ad450e..4ce22d79db 100644 --- a/routers/web/repo/view_readme.go +++ b/routers/web/repo/view_readme.go @@ -212,7 +212,7 @@ func prepareToRenderReadmeFile(ctx *context.Context, subfolder string, readmeFil ctx.Data["EscapeStatus"], ctx.Data["FileContent"] = charset.EscapeControlHTML(template.HTML(contentEscaped), ctx.Locale) } - if !fInfo.isLFSFile && ctx.Repo.CanEnableEditor(ctx, ctx.Doer) { + if !fInfo.isLFSFile && ctx.Repo.Repository.CanEnableEditor() { ctx.Data["CanEditReadmeFile"] = true } } diff --git a/routers/web/repo/wiki.go b/routers/web/repo/wiki.go index a1e10c380d..69858c9692 100644 --- a/routers/web/repo/wiki.go +++ b/routers/web/repo/wiki.go @@ -7,6 +7,7 @@ package repo import ( "bytes" gocontext "context" + "html/template" "io" "net/http" "net/url" @@ -61,9 +62,9 @@ func MustEnableWiki(ctx *context.Context) { return } - unit, err := ctx.Repo.Repository.GetUnit(ctx, unit.TypeExternalWiki) + repoUnit, err := ctx.Repo.Repository.GetUnit(ctx, unit.TypeExternalWiki) if err == nil { - ctx.Redirect(unit.ExternalWikiConfig().ExternalWikiURL) + ctx.Redirect(repoUnit.ExternalWikiConfig().ExternalWikiURL) return } } @@ -95,7 +96,7 @@ func findEntryForFile(commit *git.Commit, target string) (*git.TreeEntry, error) } func findWikiRepoCommit(ctx *context.Context) (*git.Repository, *git.Commit, error) { - wikiGitRepo, errGitRepo := gitrepo.OpenRepository(ctx, ctx.Repo.Repository.WikiStorageRepo()) + wikiGitRepo, errGitRepo := gitrepo.RepositoryFromRequestContextOrOpen(ctx, ctx.Repo.Repository.WikiStorageRepo()) if errGitRepo != nil { ctx.ServerError("OpenRepository", errGitRepo) return nil, nil, errGitRepo @@ -178,23 +179,17 @@ func wikiContentsByName(ctx *context.Context, commit *git.Commit, wikiName wiki_ } func renderViewPage(ctx *context.Context) (*git.Repository, *git.TreeEntry) { - wikiRepo, commit, err := findWikiRepoCommit(ctx) + wikiGitRepo, commit, err := findWikiRepoCommit(ctx) if err != nil { - if wikiRepo != nil { - wikiRepo.Close() - } if !git.IsErrNotExist(err) { ctx.ServerError("GetBranchCommit", err) } return nil, nil } - // Get page list. + // get the wiki pages list. entries, err := commit.ListEntries() if err != nil { - if wikiRepo != nil { - wikiRepo.Close() - } ctx.ServerError("ListEntries", err) return nil, nil } @@ -208,9 +203,6 @@ func renderViewPage(ctx *context.Context) (*git.Repository, *git.TreeEntry) { if repo_model.IsErrWikiInvalidFileName(err) { continue } - if wikiRepo != nil { - wikiRepo.Close() - } ctx.ServerError("WikiFilenameToName", err) return nil, nil } else if wikiName == "_Sidebar" || wikiName == "_Footer" { @@ -249,58 +241,26 @@ func renderViewPage(ctx *context.Context) (*git.Repository, *git.TreeEntry) { ctx.Redirect(util.URLJoin(ctx.Repo.RepoLink, "wiki/raw", string(pageName))) } if entry == nil || ctx.Written() { - if wikiRepo != nil { - wikiRepo.Close() - } return nil, nil } - // get filecontent + // get page content data := wikiContentsByEntry(ctx, entry) if ctx.Written() { - if wikiRepo != nil { - wikiRepo.Close() - } return nil, nil } - var sidebarContent []byte - if !isSideBar { - sidebarContent, _, _, _ = wikiContentsByName(ctx, commit, "_Sidebar") - if ctx.Written() { - if wikiRepo != nil { - wikiRepo.Close() - } - return nil, nil - } - } else { - sidebarContent = data - } - - var footerContent []byte - if !isFooter { - footerContent, _, _, _ = wikiContentsByName(ctx, commit, "_Footer") - if ctx.Written() { - if wikiRepo != nil { - wikiRepo.Close() - } - return nil, nil - } - } else { - footerContent = data - } - rctx := renderhelper.NewRenderContextRepoWiki(ctx, ctx.Repo.Repository) - buf := &strings.Builder{} - renderFn := func(data []byte) (escaped *charset.EscapeStatus, output string, err error) { + renderFn := func(data []byte) (escaped *charset.EscapeStatus, output template.HTML, err error) { + buf := &strings.Builder{} markupRd, markupWr := io.Pipe() defer markupWr.Close() done := make(chan struct{}) go func() { // We allow NBSP here this is rendered escaped, _ = charset.EscapeControlReader(markupRd, buf, ctx.Locale, charset.RuneNBSP) - output = buf.String() + output = template.HTML(buf.String()) buf.Reset() close(done) }() @@ -311,75 +271,61 @@ func renderViewPage(ctx *context.Context) (*git.Repository, *git.TreeEntry) { return escaped, output, err } - ctx.Data["EscapeStatus"], ctx.Data["content"], err = renderFn(data) + ctx.Data["EscapeStatus"], ctx.Data["WikiContentHTML"], err = renderFn(data) if err != nil { - if wikiRepo != nil { - wikiRepo.Close() - } ctx.ServerError("Render", err) return nil, nil } if rctx.SidebarTocNode != nil { - sb := &strings.Builder{} - err = markdown.SpecializedMarkdown(rctx).Renderer().Render(sb, nil, rctx.SidebarTocNode) - if err != nil { + sb := strings.Builder{} + if err = markdown.SpecializedMarkdown(rctx).Renderer().Render(&sb, nil, rctx.SidebarTocNode); err != nil { log.Error("Failed to render wiki sidebar TOC: %v", err) - } else { - ctx.Data["sidebarTocContent"] = sb.String() } + ctx.Data["WikiSidebarTocHTML"] = templates.SanitizeHTML(sb.String()) } if !isSideBar { - buf.Reset() - ctx.Data["sidebarEscapeStatus"], ctx.Data["sidebarContent"], err = renderFn(sidebarContent) + sidebarContent, _, _, _ := wikiContentsByName(ctx, commit, "_Sidebar") + if ctx.Written() { + return nil, nil + } + ctx.Data["WikiSidebarEscapeStatus"], ctx.Data["WikiSidebarHTML"], err = renderFn(sidebarContent) if err != nil { - if wikiRepo != nil { - wikiRepo.Close() - } ctx.ServerError("Render", err) return nil, nil } - ctx.Data["sidebarPresent"] = sidebarContent != nil - } else { - ctx.Data["sidebarPresent"] = false } if !isFooter { - buf.Reset() - ctx.Data["footerEscapeStatus"], ctx.Data["footerContent"], err = renderFn(footerContent) + footerContent, _, _, _ := wikiContentsByName(ctx, commit, "_Footer") + if ctx.Written() { + return nil, nil + } + ctx.Data["WikiFooterEscapeStatus"], ctx.Data["WikiFooterHTML"], err = renderFn(footerContent) if err != nil { - if wikiRepo != nil { - wikiRepo.Close() - } ctx.ServerError("Render", err) return nil, nil } - ctx.Data["footerPresent"] = footerContent != nil - } else { - ctx.Data["footerPresent"] = false } // get commit count - wiki revisions - commitsCount, _ := wikiRepo.FileCommitsCount(ctx.Repo.Repository.DefaultWikiBranch, pageFilename) + commitsCount, _ := wikiGitRepo.FileCommitsCount(ctx.Repo.Repository.DefaultWikiBranch, pageFilename) ctx.Data["CommitCount"] = commitsCount - return wikiRepo, entry + return wikiGitRepo, entry } func renderRevisionPage(ctx *context.Context) (*git.Repository, *git.TreeEntry) { - wikiRepo, commit, err := findWikiRepoCommit(ctx) + wikiGitRepo, commit, err := findWikiRepoCommit(ctx) if err != nil { - if wikiRepo != nil { - wikiRepo.Close() - } if !git.IsErrNotExist(err) { ctx.ServerError("GetBranchCommit", err) } return nil, nil } - // get requested pagename + // get requested page name pageName := wiki_service.WebPathFromRequest(ctx.PathParamRaw("*")) if len(pageName) == 0 { pageName = "Home" @@ -394,50 +340,35 @@ func renderRevisionPage(ctx *context.Context) (*git.Repository, *git.TreeEntry) ctx.Data["Username"] = ctx.Repo.Owner.Name ctx.Data["Reponame"] = ctx.Repo.Repository.Name - // lookup filename in wiki - get filecontent, gitTree entry , real filename - data, entry, pageFilename, noEntry := wikiContentsByName(ctx, commit, pageName) + // lookup filename in wiki - get page content, gitTree entry , real filename + _, entry, pageFilename, noEntry := wikiContentsByName(ctx, commit, pageName) if noEntry { ctx.Redirect(ctx.Repo.RepoLink + "/wiki/?action=_pages") } if entry == nil || ctx.Written() { - if wikiRepo != nil { - wikiRepo.Close() - } return nil, nil } - ctx.Data["content"] = string(data) - ctx.Data["sidebarPresent"] = false - ctx.Data["sidebarContent"] = "" - ctx.Data["footerPresent"] = false - ctx.Data["footerContent"] = "" - // get commit count - wiki revisions - commitsCount, _ := wikiRepo.FileCommitsCount(ctx.Repo.Repository.DefaultWikiBranch, pageFilename) + commitsCount, _ := wikiGitRepo.FileCommitsCount(ctx.Repo.Repository.DefaultWikiBranch, pageFilename) ctx.Data["CommitCount"] = commitsCount // get page page := max(ctx.FormInt("page"), 1) // get Commit Count - commitsHistory, err := wikiRepo.CommitsByFileAndRange( + commitsHistory, err := wikiGitRepo.CommitsByFileAndRange( git.CommitsByFileAndRangeOptions{ Revision: ctx.Repo.Repository.DefaultWikiBranch, File: pageFilename, Page: page, }) if err != nil { - if wikiRepo != nil { - wikiRepo.Close() - } ctx.ServerError("CommitsByFileAndRange", err) return nil, nil } ctx.Data["Commits"], err = git_service.ConvertFromGitCommit(ctx, commitsHistory, ctx.Repo.Repository) if err != nil { - if wikiRepo != nil { - wikiRepo.Close() - } ctx.ServerError("ConvertFromGitCommit", err) return nil, nil } @@ -446,16 +377,11 @@ func renderRevisionPage(ctx *context.Context) (*git.Repository, *git.TreeEntry) pager.AddParamFromRequest(ctx.Req) ctx.Data["Page"] = pager - return wikiRepo, entry + return wikiGitRepo, entry } func renderEditPage(ctx *context.Context) { - wikiRepo, commit, err := findWikiRepoCommit(ctx) - defer func() { - if wikiRepo != nil { - _ = wikiRepo.Close() - } - }() + _, commit, err := findWikiRepoCommit(ctx) if err != nil { if !git.IsErrNotExist(err) { ctx.ServerError("GetBranchCommit", err) @@ -463,7 +389,7 @@ func renderEditPage(ctx *context.Context) { return } - // get requested pagename + // get requested page name pageName := wiki_service.WebPathFromRequest(ctx.PathParamRaw("*")) if len(pageName) == 0 { pageName = "Home" @@ -487,17 +413,13 @@ func renderEditPage(ctx *context.Context) { return } - // get filecontent + // get wiki page content data := wikiContentsByEntry(ctx, entry) if ctx.Written() { return } - ctx.Data["content"] = string(data) - ctx.Data["sidebarPresent"] = false - ctx.Data["sidebarContent"] = "" - ctx.Data["footerPresent"] = false - ctx.Data["footerContent"] = "" + ctx.Data["WikiEditContent"] = string(data) } // WikiPost renders post of wiki page @@ -559,12 +481,7 @@ func Wiki(ctx *context.Context) { return } - wikiRepo, entry := renderViewPage(ctx) - defer func() { - if wikiRepo != nil { - wikiRepo.Close() - } - }() + wikiGitRepo, entry := renderViewPage(ctx) if ctx.Written() { return } @@ -580,7 +497,7 @@ func Wiki(ctx *context.Context) { ctx.Data["FormatWarning"] = ext + " rendering is not supported at the moment. Rendered as Markdown." } // Get last change information. - lastCommit, err := wikiRepo.GetCommitByPath(wikiPath) + lastCommit, err := wikiGitRepo.GetCommitByPath(wikiPath) if err != nil { ctx.ServerError("GetCommitByPath", err) return @@ -600,13 +517,7 @@ func WikiRevision(ctx *context.Context) { return } - wikiRepo, entry := renderRevisionPage(ctx) - defer func() { - if wikiRepo != nil { - wikiRepo.Close() - } - }() - + wikiGitRepo, entry := renderRevisionPage(ctx) if ctx.Written() { return } @@ -618,7 +529,7 @@ func WikiRevision(ctx *context.Context) { // Get last change information. wikiPath := entry.Name() - lastCommit, err := wikiRepo.GetCommitByPath(wikiPath) + lastCommit, err := wikiGitRepo.GetCommitByPath(wikiPath) if err != nil { ctx.ServerError("GetCommitByPath", err) return @@ -638,12 +549,7 @@ func WikiPages(ctx *context.Context) { ctx.Data["Title"] = ctx.Tr("repo.wiki.pages") ctx.Data["CanWriteWiki"] = ctx.Repo.CanWrite(unit.TypeWiki) && !ctx.Repo.Repository.IsArchived - wikiRepo, commit, err := findWikiRepoCommit(ctx) - defer func() { - if wikiRepo != nil { - _ = wikiRepo.Close() - } - }() + _, commit, err := findWikiRepoCommit(ctx) if err != nil { ctx.Redirect(ctx.Repo.RepoLink + "/wiki") return @@ -697,13 +603,7 @@ func WikiPages(ctx *context.Context) { // WikiRaw outputs raw blob requested by user (image for example) func WikiRaw(ctx *context.Context) { - wikiRepo, commit, err := findWikiRepoCommit(ctx) - defer func() { - if wikiRepo != nil { - wikiRepo.Close() - } - }() - + _, commit, err := findWikiRepoCommit(ctx) if err != nil { if git.IsErrNotExist(err) { ctx.NotFound(nil) diff --git a/routers/web/repo/wiki_test.go b/routers/web/repo/wiki_test.go index 73f9970a07..59bf6ed79b 100644 --- a/routers/web/repo/wiki_test.go +++ b/routers/web/repo/wiki_test.go @@ -164,7 +164,7 @@ func TestEditWiki(t *testing.T) { EditWiki(ctx) assert.Equal(t, http.StatusOK, ctx.Resp.WrittenStatus()) assert.EqualValues(t, "Home", ctx.Data["Title"]) - assert.Equal(t, wikiContent(t, ctx.Repo.Repository, "Home"), ctx.Data["content"]) + assert.Equal(t, wikiContent(t, ctx.Repo.Repository, "Home"), ctx.Data["WikiEditContent"]) ctx, _ = contexttest.MockContext(t, "user2/repo1/wiki/jpeg.jpg?action=_edit") ctx.SetPathParam("*", "jpeg.jpg") diff --git a/routers/web/user/profile.go b/routers/web/user/profile.go index f6d50cf5fe..d7052914b6 100644 --- a/routers/web/user/profile.go +++ b/routers/web/user/profile.go @@ -197,6 +197,7 @@ func prepareUserProfileTabData(ctx *context.Context, profileDbRepo *repo_model.R total = int(count) case "stars": ctx.Data["PageIsProfileStarList"] = true + ctx.Data["ShowRepoOwnerOnList"] = true repos, count, err = repo_model.SearchRepository(ctx, repo_model.SearchRepoOptions{ ListOptions: db.ListOptions{ PageSize: pagingNum, diff --git a/routers/web/web.go b/routers/web/web.go index a54f96ec68..4b5d68b260 100644 --- a/routers/web/web.go +++ b/routers/web/web.go @@ -964,7 +964,8 @@ func registerWebRoutes(m *web.Router) { addSettingsVariablesRoutes() }, actions.MustEnableActions) - m.Methods("GET,POST", "/delete", org.SettingsDelete) + m.Post("/rename", web.Bind(forms.RenameOrgForm{}), org.SettingsRenamePost) + m.Post("/delete", org.SettingsDeleteOrgPost) m.Group("/packages", func() { m.Get("", org.Packages) @@ -1312,26 +1313,38 @@ func registerWebRoutes(m *web.Router) { }, reqSignIn, context.RepoAssignment, context.RepoMustNotBeArchived()) // end "/{username}/{reponame}": create or edit issues, pulls, labels, milestones - m.Group("/{username}/{reponame}", func() { // repo code + m.Group("/{username}/{reponame}", func() { // repo code (at least "code reader") m.Group("", func() { m.Group("", func() { - m.Post("/_preview/*", web.Bind(forms.EditPreviewDiffForm{}), repo.DiffPreviewPost) - m.Combo("/_edit/*").Get(repo.EditFile). - Post(web.Bind(forms.EditRepoFileForm{}), repo.EditFilePost) - m.Combo("/_new/*").Get(repo.NewFile). - Post(web.Bind(forms.EditRepoFileForm{}), repo.NewFilePost) - m.Combo("/_delete/*").Get(repo.DeleteFile). - Post(web.Bind(forms.DeleteRepoFileForm{}), repo.DeleteFilePost) - m.Combo("/_upload/*", repo.MustBeAbleToUpload).Get(repo.UploadFile). - Post(web.Bind(forms.UploadRepoFileForm{}), repo.UploadFilePost) - m.Combo("/_diffpatch/*").Get(repo.NewDiffPatch). - Post(web.Bind(forms.EditRepoFileForm{}), repo.NewDiffPatchPost) - m.Combo("/_cherrypick/{sha:([a-f0-9]{7,64})}/*").Get(repo.CherryPick). - Post(web.Bind(forms.CherryPickForm{}), repo.CherryPickPost) - }, context.RepoRefByType(git.RefTypeBranch), context.CanWriteToBranch(), repo.WebGitOperationCommonData) + // "GET" requests only need "code reader" permission, "POST" requests need "code writer" permission. + // Because reader can "fork and edit" + canWriteToBranch := context.CanWriteToBranch() + m.Post("/_preview/*", repo.DiffPreviewPost) // read-only, fine with "code reader" + m.Post("/_fork/*", repo.ForkToEditPost) // read-only, fork to own repo, fine with "code reader" + + // the path params are used in PrepareCommitFormOptions to construct the correct form action URL + m.Combo("/{editor_action:_edit}/*"). + Get(repo.EditFile). + Post(web.Bind(forms.EditRepoFileForm{}), canWriteToBranch, repo.EditFilePost) + m.Combo("/{editor_action:_new}/*"). + Get(repo.EditFile). + Post(web.Bind(forms.EditRepoFileForm{}), canWriteToBranch, repo.EditFilePost) + m.Combo("/{editor_action:_delete}/*"). + Get(repo.DeleteFile). + Post(web.Bind(forms.DeleteRepoFileForm{}), canWriteToBranch, repo.DeleteFilePost) + m.Combo("/{editor_action:_upload}/*", repo.MustBeAbleToUpload). + Get(repo.UploadFile). + Post(web.Bind(forms.UploadRepoFileForm{}), canWriteToBranch, repo.UploadFilePost) + m.Combo("/{editor_action:_diffpatch}/*"). + Get(repo.NewDiffPatch). + Post(web.Bind(forms.EditRepoFileForm{}), canWriteToBranch, repo.NewDiffPatchPost) + m.Combo("/{editor_action:_cherrypick}/{sha:([a-f0-9]{7,64})}/*"). + Get(repo.CherryPick). + Post(web.Bind(forms.CherryPickForm{}), canWriteToBranch, repo.CherryPickPost) + }, context.RepoRefByType(git.RefTypeBranch), repo.WebGitOperationCommonData) m.Group("", func() { m.Post("/upload-file", repo.UploadFileToServer) - m.Post("/upload-remove", web.Bind(forms.RemoveUploadFileForm{}), repo.RemoveUploadFileFromServer) + m.Post("/upload-remove", repo.RemoveUploadFileFromServer) }, repo.MustBeAbleToUpload, reqRepoCodeWriter) }, repo.MustBeEditable, context.RepoMustNotBeArchived()) diff --git a/services/context/repo.go b/services/context/repo.go index 32d54c88ff..572211712b 100644 --- a/services/context/repo.go +++ b/services/context/repo.go @@ -71,11 +71,6 @@ func (r *Repository) CanWriteToBranch(ctx context.Context, user *user_model.User return issues_model.CanMaintainerWriteToBranch(ctx, r.Permission, branch, user) } -// CanEnableEditor returns true if repository is editable and user has proper access level. -func (r *Repository) CanEnableEditor(ctx context.Context, user *user_model.User) bool { - return r.RefFullName.IsBranch() && r.CanWriteToBranch(ctx, user, r.BranchName) && r.Repository.CanEnableEditor() && !r.Repository.IsArchived -} - // CanCreateBranch returns true if repository is editable and user has proper access level. func (r *Repository) CanCreateBranch() bool { return r.Permission.CanWrite(unit_model.TypeCode) && r.Repository.CanCreateBranch() @@ -94,59 +89,100 @@ func RepoMustNotBeArchived() func(ctx *Context) { } } -// CanCommitToBranchResults represents the results of CanCommitToBranch -type CanCommitToBranchResults struct { - CanCommitToBranch bool - EditorEnabled bool - UserCanPush bool - RequireSigned bool - WillSign bool - SigningKey *git.SigningKey - WontSignReason string +type CommitFormOptions struct { + NeedFork bool + + TargetRepo *repo_model.Repository + TargetFormAction string + WillSubmitToFork bool + CanCommitToBranch bool + UserCanPush bool + RequireSigned bool + WillSign bool + SigningKey *git.SigningKey + WontSignReason string + CanCreatePullRequest bool + CanCreateBasePullRequest bool } -// CanCommitToBranch returns true if repository is editable and user has proper access level -// -// and branch is not protected for push -func (r *Repository) CanCommitToBranch(ctx context.Context, doer *user_model.User) (CanCommitToBranchResults, error) { - protectedBranch, err := git_model.GetFirstMatchProtectedBranchRule(ctx, r.Repository.ID, r.BranchName) +func PrepareCommitFormOptions(ctx *Context, doer *user_model.User, targetRepo *repo_model.Repository, doerRepoPerm access_model.Permission, refName git.RefName) (*CommitFormOptions, error) { + if !refName.IsBranch() { + // it shouldn't happen because middleware already checks + return nil, util.NewInvalidArgumentErrorf("ref %q is not a branch", refName) + } + + originRepo := targetRepo + branchName := refName.ShortName() + // TODO: CanMaintainerWriteToBranch is a bad name, but it really does what "CanWriteToBranch" does + if !issues_model.CanMaintainerWriteToBranch(ctx, doerRepoPerm, branchName, doer) { + targetRepo = repo_model.GetForkedRepo(ctx, doer.ID, targetRepo.ID) + if targetRepo == nil { + return &CommitFormOptions{NeedFork: true}, nil + } + // now, we get our own forked repo; it must be writable by us. + } + submitToForkedRepo := targetRepo.ID != originRepo.ID + err := targetRepo.GetBaseRepo(ctx) + if err != nil { + return nil, err + } + + protectedBranch, err := git_model.GetFirstMatchProtectedBranchRule(ctx, targetRepo.ID, branchName) if err != nil { - return CanCommitToBranchResults{}, err + return nil, err } - userCanPush := true - requireSigned := false + canPushWithProtection := true + protectionRequireSigned := false if protectedBranch != nil { - protectedBranch.Repo = r.Repository - userCanPush = protectedBranch.CanUserPush(ctx, doer) - requireSigned = protectedBranch.RequireSignedCommits + protectedBranch.Repo = targetRepo + canPushWithProtection = protectedBranch.CanUserPush(ctx, doer) + protectionRequireSigned = protectedBranch.RequireSignedCommits } - sign, keyID, _, err := asymkey_service.SignCRUDAction(ctx, r.Repository.RepoPath(), doer, r.Repository.RepoPath(), git.BranchPrefix+r.BranchName) - - canEnableEditor := r.CanEnableEditor(ctx, doer) - canCommit := canEnableEditor && userCanPush - if requireSigned { - canCommit = canCommit && sign - } + willSign, signKeyID, _, err := asymkey_service.SignCRUDAction(ctx, targetRepo.RepoPath(), doer, targetRepo.RepoPath(), refName.String()) wontSignReason := "" - if err != nil { - if asymkey_service.IsErrWontSign(err) { - wontSignReason = string(err.(*asymkey_service.ErrWontSign).Reason) - err = nil - } else { - wontSignReason = "error" - } + if asymkey_service.IsErrWontSign(err) { + wontSignReason = string(err.(*asymkey_service.ErrWontSign).Reason) + } else if err != nil { + return nil, err + } + + canCommitToBranch := !submitToForkedRepo /* same repo */ && targetRepo.CanEnableEditor() && canPushWithProtection + if protectionRequireSigned { + canCommitToBranch = canCommitToBranch && willSign } - return CanCommitToBranchResults{ - CanCommitToBranch: canCommit, - EditorEnabled: canEnableEditor, - UserCanPush: userCanPush, - RequireSigned: requireSigned, - WillSign: sign, - SigningKey: keyID, + canCreateBasePullRequest := targetRepo.BaseRepo != nil && targetRepo.BaseRepo.UnitEnabled(ctx, unit_model.TypePullRequests) + canCreatePullRequest := targetRepo.UnitEnabled(ctx, unit_model.TypePullRequests) || canCreateBasePullRequest + + opts := &CommitFormOptions{ + TargetRepo: targetRepo, + WillSubmitToFork: submitToForkedRepo, + CanCommitToBranch: canCommitToBranch, + UserCanPush: canPushWithProtection, + RequireSigned: protectionRequireSigned, + WillSign: willSign, + SigningKey: signKeyID, WontSignReason: wontSignReason, - }, err + + CanCreatePullRequest: canCreatePullRequest, + CanCreateBasePullRequest: canCreateBasePullRequest, + } + 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. diff --git a/services/context/upload/upload.go b/services/context/upload/upload.go index 5edddc6f27..23707950d4 100644 --- a/services/context/upload/upload.go +++ b/services/context/upload/upload.go @@ -11,7 +11,9 @@ import ( "regexp" "strings" + repo_model "code.gitea.io/gitea/models/repo" "code.gitea.io/gitea/modules/log" + "code.gitea.io/gitea/modules/reqctx" "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/services/context" ) @@ -106,12 +108,17 @@ func AddUploadContext(ctx *context.Context, uploadType string) { ctx.Data["UploadAccepts"] = strings.ReplaceAll(setting.Attachment.AllowedTypes, "|", ",") ctx.Data["UploadMaxFiles"] = setting.Attachment.MaxFiles ctx.Data["UploadMaxSize"] = setting.Attachment.MaxSize - case "repo": - ctx.Data["UploadUrl"] = ctx.Repo.RepoLink + "/upload-file" - ctx.Data["UploadRemoveUrl"] = ctx.Repo.RepoLink + "/upload-remove" - ctx.Data["UploadLinkUrl"] = ctx.Repo.RepoLink + "/upload-file" - ctx.Data["UploadAccepts"] = strings.ReplaceAll(setting.Repository.Upload.AllowedTypes, "|", ",") - ctx.Data["UploadMaxFiles"] = setting.Repository.Upload.MaxFiles - ctx.Data["UploadMaxSize"] = setting.Repository.Upload.FileMaxSize + default: + setting.PanicInDevOrTesting("Invalid upload type: %s", uploadType) } } + +func AddUploadContextForRepo(ctx reqctx.RequestContext, repo *repo_model.Repository) { + ctxData, repoLink := ctx.GetData(), repo.Link() + ctxData["UploadUrl"] = repoLink + "/upload-file" + ctxData["UploadRemoveUrl"] = repoLink + "/upload-remove" + ctxData["UploadLinkUrl"] = repoLink + "/upload-file" + ctxData["UploadAccepts"] = strings.ReplaceAll(setting.Repository.Upload.AllowedTypes, "|", ",") + ctxData["UploadMaxFiles"] = setting.Repository.Upload.MaxFiles + ctxData["UploadMaxSize"] = setting.Repository.Upload.FileMaxSize +} diff --git a/services/forms/org.go b/services/forms/org.go index db182f7e96..2ac18ef25c 100644 --- a/services/forms/org.go +++ b/services/forms/org.go @@ -36,7 +36,6 @@ func (f *CreateOrgForm) Validate(req *http.Request, errs binding.Errors) binding // UpdateOrgSettingForm form for updating organization settings type UpdateOrgSettingForm struct { - Name string `binding:"Required;Username;MaxSize(40)" locale:"org.org_name_holder"` FullName string `binding:"MaxSize(100)"` Email string `binding:"MaxSize(255)"` Description string `binding:"MaxSize(255)"` @@ -53,6 +52,11 @@ func (f *UpdateOrgSettingForm) Validate(req *http.Request, errs binding.Errors) return middleware.Validate(errs, ctx.Data, f, ctx.Locale) } +type RenameOrgForm struct { + OrgName string `binding:"Required"` + NewOrgName string `binding:"Required;Username;MaxSize(40)" locale:"org.org_name_holder"` +} + // ___________ // \__ ___/___ _____ _____ // | |_/ __ \\__ \ / \ diff --git a/services/forms/repo_form.go b/services/forms/repo_form.go index c79d3b95e7..d116bb9f11 100644 --- a/services/forms/repo_form.go +++ b/services/forms/repo_form.go @@ -10,7 +10,6 @@ import ( issues_model "code.gitea.io/gitea/models/issues" project_model "code.gitea.io/gitea/models/project" - "code.gitea.io/gitea/modules/optional" "code.gitea.io/gitea/modules/structs" "code.gitea.io/gitea/modules/web/middleware" "code.gitea.io/gitea/services/context" @@ -681,129 +680,6 @@ func (f *NewWikiForm) Validate(req *http.Request, errs binding.Errors) binding.E return middleware.Validate(errs, ctx.Data, f, ctx.Locale) } -// ___________ .___.__ __ -// \_ _____/ __| _/|__|/ |_ -// | __)_ / __ | | \ __\ -// | \/ /_/ | | || | -// /_______ /\____ | |__||__| -// \/ \/ - -// EditRepoFileForm form for changing repository file -type EditRepoFileForm struct { - TreePath string `binding:"Required;MaxSize(500)"` - Content optional.Option[string] - CommitSummary string `binding:"MaxSize(100)"` - CommitMessage string - CommitChoice string `binding:"Required;MaxSize(50)"` - NewBranchName string `binding:"GitRefName;MaxSize(100)"` - LastCommit string - Signoff bool - CommitEmail string -} - -// Validate validates the fields -func (f *EditRepoFileForm) Validate(req *http.Request, errs binding.Errors) binding.Errors { - ctx := context.GetValidateContext(req) - return middleware.Validate(errs, ctx.Data, f, ctx.Locale) -} - -// EditPreviewDiffForm form for changing preview diff -type EditPreviewDiffForm struct { - Content string -} - -// Validate validates the fields -func (f *EditPreviewDiffForm) Validate(req *http.Request, errs binding.Errors) binding.Errors { - ctx := context.GetValidateContext(req) - return middleware.Validate(errs, ctx.Data, f, ctx.Locale) -} - -// _________ .__ __________.__ __ -// \_ ___ \| |__ __________________ ___.__. \______ \__| ____ | | __ -// / \ \/| | \_/ __ \_ __ \_ __ < | | | ___/ |/ ___\| |/ / -// \ \___| Y \ ___/| | \/| | \/\___ | | | | \ \___| < -// \______ /___| /\___ >__| |__| / ____| |____| |__|\___ >__|_ \ -// \/ \/ \/ \/ \/ \/ - -// CherryPickForm form for changing repository file -type CherryPickForm struct { - CommitSummary string `binding:"MaxSize(100)"` - CommitMessage string - CommitChoice string `binding:"Required;MaxSize(50)"` - NewBranchName string `binding:"GitRefName;MaxSize(100)"` - LastCommit string - Revert bool - Signoff bool - CommitEmail string -} - -// Validate validates the fields -func (f *CherryPickForm) Validate(req *http.Request, errs binding.Errors) binding.Errors { - ctx := context.GetValidateContext(req) - return middleware.Validate(errs, ctx.Data, f, ctx.Locale) -} - -// ____ ___ .__ .___ -// | | \______ | | _________ __| _/ -// | | /\____ \| | / _ \__ \ / __ | -// | | / | |_> > |_( <_> ) __ \_/ /_/ | -// |______/ | __/|____/\____(____ /\____ | -// |__| \/ \/ -// - -// UploadRepoFileForm form for uploading repository file -type UploadRepoFileForm struct { - TreePath string `binding:"MaxSize(500)"` - CommitSummary string `binding:"MaxSize(100)"` - CommitMessage string - CommitChoice string `binding:"Required;MaxSize(50)"` - NewBranchName string `binding:"GitRefName;MaxSize(100)"` - Files []string - Signoff bool - CommitEmail string -} - -// Validate validates the fields -func (f *UploadRepoFileForm) Validate(req *http.Request, errs binding.Errors) binding.Errors { - ctx := context.GetValidateContext(req) - return middleware.Validate(errs, ctx.Data, f, ctx.Locale) -} - -// RemoveUploadFileForm form for removing uploaded file -type RemoveUploadFileForm struct { - File string `binding:"Required;MaxSize(50)"` -} - -// Validate validates the fields -func (f *RemoveUploadFileForm) Validate(req *http.Request, errs binding.Errors) binding.Errors { - ctx := context.GetValidateContext(req) - return middleware.Validate(errs, ctx.Data, f, ctx.Locale) -} - -// ________ .__ __ -// \______ \ ____ | | _____/ |_ ____ -// | | \_/ __ \| | _/ __ \ __\/ __ \ -// | ` \ ___/| |_\ ___/| | \ ___/ -// /_______ /\___ >____/\___ >__| \___ > -// \/ \/ \/ \/ - -// DeleteRepoFileForm form for deleting repository file -type DeleteRepoFileForm struct { - CommitSummary string `binding:"MaxSize(100)"` - CommitMessage string - CommitChoice string `binding:"Required;MaxSize(50)"` - NewBranchName string `binding:"GitRefName;MaxSize(100)"` - LastCommit string - Signoff bool - CommitEmail string -} - -// Validate validates the fields -func (f *DeleteRepoFileForm) Validate(req *http.Request, errs binding.Errors) binding.Errors { - ctx := context.GetValidateContext(req) - return middleware.Validate(errs, ctx.Data, f, ctx.Locale) -} - // ___________.__ ___________ __ // \__ ___/|__| _____ ____ \__ ___/___________ ____ | | __ ___________ // | | | |/ \_/ __ \ | | \_ __ \__ \ _/ ___\| |/ // __ \_ __ \ diff --git a/services/forms/repo_form_editor.go b/services/forms/repo_form_editor.go new file mode 100644 index 0000000000..3ad2eae75d --- /dev/null +++ b/services/forms/repo_form_editor.go @@ -0,0 +1,57 @@ +// Copyright 2025 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package forms + +import ( + "net/http" + + "code.gitea.io/gitea/modules/optional" + "code.gitea.io/gitea/modules/web/middleware" + "code.gitea.io/gitea/services/context" + + "gitea.com/go-chi/binding" +) + +type CommitCommonForm struct { + TreePath string `binding:"MaxSize(500)"` + CommitSummary string `binding:"MaxSize(100)"` + CommitMessage string + CommitChoice string `binding:"Required;MaxSize(50)"` + NewBranchName string `binding:"GitRefName;MaxSize(100)"` + LastCommit string + Signoff bool + CommitEmail string +} + +func (f *CommitCommonForm) Validate(req *http.Request, errs binding.Errors) binding.Errors { + ctx := context.GetValidateContext(req) + return middleware.Validate(errs, ctx.Data, f, ctx.Locale) +} + +type CommitCommonFormInterface interface { + GetCommitCommonForm() *CommitCommonForm +} + +func (f *CommitCommonForm) GetCommitCommonForm() *CommitCommonForm { + return f +} + +type EditRepoFileForm struct { + CommitCommonForm + Content optional.Option[string] +} + +type DeleteRepoFileForm struct { + CommitCommonForm +} + +type UploadRepoFileForm struct { + CommitCommonForm + Files []string +} + +type CherryPickForm struct { + CommitCommonForm + Revert bool +} diff --git a/services/mailer/mail_test.go b/services/mailer/mail_test.go index 7a47cf3876..b15949f352 100644 --- a/services/mailer/mail_test.go +++ b/services/mailer/mail_test.go @@ -528,7 +528,7 @@ func TestEmbedBase64Images(t *testing.T) { require.NoError(t, err) mailBody := msgs[0].Body - assert.Regexp(t, `MSG-BEFORE <a[^>]+><img src="data:image/png;base64,iVBORw0KGgo="/></a> MSG-AFTER`, mailBody) + assert.Regexp(t, `MSG-BEFORE <a[^>]+><img src="data:image/png;base64,iVBORw0KGgo=".*/></a> MSG-AFTER`, mailBody) }) t.Run("EmbedInstanceImageSkipExternalImage", func(t *testing.T) { diff --git a/services/packages/container/cleanup.go b/services/packages/container/cleanup.go index d15d6b6c84..263562a396 100644 --- a/services/packages/container/cleanup.go +++ b/services/packages/container/cleanup.go @@ -13,7 +13,7 @@ import ( container_module "code.gitea.io/gitea/modules/packages/container" packages_service "code.gitea.io/gitea/services/packages" - digest "github.com/opencontainers/go-digest" + "github.com/opencontainers/go-digest" ) // Cleanup removes expired container data diff --git a/services/pull/commit_status.go b/services/pull/commit_status.go index d3a0f718a7..7952ca6fe3 100644 --- a/services/pull/commit_status.go +++ b/services/pull/commit_status.go @@ -38,20 +38,23 @@ func MergeRequiredContextsCommitStatus(commitStatuses []*git_model.CommitStatus, } requiredCommitStatuses := make([]*git_model.CommitStatus, 0, len(commitStatuses)) + allRequiredContextsMatched := true for _, gp := range requiredContextsGlob { + requiredContextMatched := false for _, commitStatus := range commitStatuses { if gp.Match(commitStatus.Context) { requiredCommitStatuses = append(requiredCommitStatuses, commitStatus) - break + requiredContextMatched = true } } + allRequiredContextsMatched = allRequiredContextsMatched && requiredContextMatched } if len(requiredCommitStatuses) == 0 { return commitstatus.CommitStatusPending } returnedStatus := git_model.CalcCommitStatus(requiredCommitStatuses).State - if len(requiredCommitStatuses) == len(requiredContexts) { + if allRequiredContextsMatched { return returnedStatus } diff --git a/services/pull/commit_status_test.go b/services/pull/commit_status_test.go index b985a9de8e..a58e788c04 100644 --- a/services/pull/commit_status_test.go +++ b/services/pull/commit_status_test.go @@ -62,6 +62,15 @@ func TestMergeRequiredContextsCommitStatus(t *testing.T) { commitStatuses: []*git_model.CommitStatus{ {Context: "Build 1", State: commitstatus.CommitStatusSuccess}, {Context: "Build 2", State: commitstatus.CommitStatusSuccess}, + {Context: "Build 2t", State: commitstatus.CommitStatusFailure}, + }, + requiredContexts: []string{"Build*"}, + expected: commitstatus.CommitStatusFailure, + }, + { + commitStatuses: []*git_model.CommitStatus{ + {Context: "Build 1", State: commitstatus.CommitStatusSuccess}, + {Context: "Build 2", State: commitstatus.CommitStatusSuccess}, {Context: "Build 2t", State: commitstatus.CommitStatusSuccess}, }, requiredContexts: []string{"Build*", "Build 2t*", "Build 3*"}, diff --git a/services/repository/files/content.go b/services/repository/files/content.go index 7a07a0ddca..ccba3b7594 100644 --- a/services/repository/files/content.go +++ b/services/repository/files/content.go @@ -42,7 +42,7 @@ func GetContentsOrList(ctx context.Context, repo *repo_model.Repository, refComm } // Check that the path given in opts.treePath is valid (not a git path) - cleanTreePath := CleanUploadFileName(treePath) + cleanTreePath := CleanGitTreePath(treePath) if cleanTreePath == "" && treePath != "" { return nil, ErrFilenameInvalid{ Path: treePath, @@ -103,7 +103,7 @@ func GetObjectTypeFromTreeEntry(entry *git.TreeEntry) ContentType { // GetContents gets the metadata on a file's contents. Ref can be a branch, commit or tag func GetContents(ctx context.Context, repo *repo_model.Repository, refCommit *utils.RefCommit, treePath string, forList bool) (*api.ContentsResponse, error) { // Check that the path given in opts.treePath is valid (not a git path) - cleanTreePath := CleanUploadFileName(treePath) + cleanTreePath := CleanGitTreePath(treePath) if cleanTreePath == "" && treePath != "" { return nil, ErrFilenameInvalid{ Path: treePath, diff --git a/services/repository/files/file.go b/services/repository/files/file.go index 0e1100a098..855dc5c8ed 100644 --- a/services/repository/files/file.go +++ b/services/repository/files/file.go @@ -134,9 +134,8 @@ func (err ErrFilenameInvalid) Unwrap() error { return util.ErrInvalidArgument } -// CleanUploadFileName Trims a filename and returns empty string if it is a .git directory -func CleanUploadFileName(name string) string { - // Rebase the filename +// CleanGitTreePath cleans a tree path for git, it returns an empty string the path is invalid (e.g.: contains ".git" part) +func CleanGitTreePath(name string) string { name = util.PathJoinRel(name) // Git disallows any filenames to have a .git directory in them. for part := range strings.SplitSeq(name, "/") { @@ -144,5 +143,8 @@ func CleanUploadFileName(name string) string { return "" } } + if name == "." { + name = "" + } return name } diff --git a/services/repository/files/file_test.go b/services/repository/files/file_test.go index 169cafba0d..894c184472 100644 --- a/services/repository/files/file_test.go +++ b/services/repository/files/file_test.go @@ -10,17 +10,9 @@ import ( ) func TestCleanUploadFileName(t *testing.T) { - t.Run("Clean regular file", func(t *testing.T) { - name := "this/is/test" - cleanName := CleanUploadFileName(name) - expectedCleanName := name - assert.Equal(t, expectedCleanName, cleanName) - }) - - t.Run("Clean a .git path", func(t *testing.T) { - name := "this/is/test/.git" - cleanName := CleanUploadFileName(name) - expectedCleanName := "" - assert.Equal(t, expectedCleanName, cleanName) - }) + assert.Equal(t, "", CleanGitTreePath("")) //nolint + assert.Equal(t, "", CleanGitTreePath(".")) //nolint + assert.Equal(t, "a/b", CleanGitTreePath("a/b")) + assert.Equal(t, "", CleanGitTreePath(".git/b")) //nolint + assert.Equal(t, "", CleanGitTreePath("a/.git")) //nolint } diff --git a/services/repository/files/update.go b/services/repository/files/update.go index 99c1215c9f..5aaa394e9a 100644 --- a/services/repository/files/update.go +++ b/services/repository/files/update.go @@ -88,8 +88,26 @@ func (err ErrRepoFileDoesNotExist) Unwrap() error { return util.ErrNotExist } +type LazyReadSeeker interface { + io.ReadSeeker + io.Closer + OpenLazyReader() error +} + // ChangeRepoFiles adds, updates or removes multiple files in the given repository -func ChangeRepoFiles(ctx context.Context, repo *repo_model.Repository, doer *user_model.User, opts *ChangeRepoFilesOptions) (*structs.FilesResponse, error) { +func ChangeRepoFiles(ctx context.Context, repo *repo_model.Repository, doer *user_model.User, opts *ChangeRepoFilesOptions) (_ *structs.FilesResponse, errRet error) { + var addedLfsPointers []lfs.Pointer + defer func() { + if errRet != nil { + for _, lfsPointer := range addedLfsPointers { + _, err := git_model.RemoveLFSMetaObjectByOid(ctx, repo.ID, lfsPointer.Oid) + if err != nil { + log.Error("ChangeRepoFiles: RemoveLFSMetaObjectByOid failed: %v", err) + } + } + } + }() + err := repo.MustNotBeArchived() if err != nil { return nil, err @@ -127,14 +145,14 @@ func ChangeRepoFiles(ctx context.Context, repo *repo_model.Repository, doer *use } // Check that the path given in opts.treePath is valid (not a git path) - treePath := CleanUploadFileName(file.TreePath) + treePath := CleanGitTreePath(file.TreePath) if treePath == "" { return nil, ErrFilenameInvalid{ Path: file.TreePath, } } // If there is a fromTreePath (we are copying it), also clean it up - fromTreePath := CleanUploadFileName(file.FromTreePath) + fromTreePath := CleanGitTreePath(file.FromTreePath) if fromTreePath == "" && file.FromTreePath != "" { return nil, ErrFilenameInvalid{ Path: file.FromTreePath, @@ -241,10 +259,14 @@ func ChangeRepoFiles(ctx context.Context, repo *repo_model.Repository, doer *use lfsContentStore := lfs.NewContentStore() for _, file := range opts.Files { switch file.Operation { - case "create", "update", "rename": - if err = CreateUpdateRenameFile(ctx, t, file, lfsContentStore, repo.ID, hasOldBranch); err != nil { + case "create", "update", "rename", "upload": + addedLfsPointer, err := modifyFile(ctx, t, file, lfsContentStore, repo.ID) + if err != nil { return nil, err } + if addedLfsPointer != nil { + addedLfsPointers = append(addedLfsPointers, *addedLfsPointer) + } case "delete": if err = t.RemoveFilesFromIndex(ctx, file.TreePath); err != nil { return nil, err @@ -366,18 +388,29 @@ func (err ErrSHAOrCommitIDNotProvided) Error() string { // handles the check for various issues for ChangeRepoFiles func handleCheckErrors(file *ChangeRepoFile, commit *git.Commit, opts *ChangeRepoFilesOptions) error { - if file.Operation == "update" || file.Operation == "delete" || file.Operation == "rename" { - fromEntry, err := commit.GetTreeEntryByPath(file.Options.fromTreePath) - if err != nil { - return err + // check old entry (fromTreePath/fromEntry) + if file.Operation == "update" || file.Operation == "upload" || file.Operation == "delete" || file.Operation == "rename" { + var fromEntryIDString string + { + fromEntry, err := commit.GetTreeEntryByPath(file.Options.fromTreePath) + if file.Operation == "upload" && git.IsErrNotExist(err) { + fromEntry = nil + } else if err != nil { + return err + } + if fromEntry != nil { + fromEntryIDString = fromEntry.ID.String() + file.Options.executable = fromEntry.IsExecutable() // FIXME: legacy hacky approach, it shouldn't prepare the "Options" in the "check" function + } } + if file.SHA != "" { // If the SHA given doesn't match the SHA of the fromTreePath, throw error - if file.SHA != fromEntry.ID.String() { + if file.SHA != fromEntryIDString { return pull_service.ErrSHADoesNotMatch{ Path: file.Options.treePath, GivenSHA: file.SHA, - CurrentSHA: fromEntry.ID.String(), + CurrentSHA: fromEntryIDString, } } } else if opts.LastCommitID != "" { @@ -399,11 +432,10 @@ func handleCheckErrors(file *ChangeRepoFile, commit *git.Commit, opts *ChangeRep // haven't been made. We throw an error if one wasn't provided. return ErrSHAOrCommitIDNotProvided{} } - // FIXME: legacy hacky approach, it shouldn't prepare the "Options" in the "check" function - file.Options.executable = fromEntry.IsExecutable() } - if file.Operation == "create" || file.Operation == "update" || file.Operation == "rename" { + // check new entry (treePath/treeEntry) + if file.Operation == "create" || file.Operation == "update" || file.Operation == "upload" || file.Operation == "rename" { // For operation's target path, we need to make sure no parts of the path are existing files or links // except for the last item in the path (which is the file name). // And that shouldn't exist IF it is a new file OR is being moved to a new path. @@ -454,18 +486,23 @@ func handleCheckErrors(file *ChangeRepoFile, commit *git.Commit, opts *ChangeRep return nil } -func CreateUpdateRenameFile(ctx context.Context, t *TemporaryUploadRepository, file *ChangeRepoFile, contentStore *lfs.ContentStore, repoID int64, hasOldBranch bool) error { +func modifyFile(ctx context.Context, t *TemporaryUploadRepository, file *ChangeRepoFile, contentStore *lfs.ContentStore, repoID int64) (addedLfsPointer *lfs.Pointer, _ error) { + if rd, ok := file.ContentReader.(LazyReadSeeker); ok { + if err := rd.OpenLazyReader(); err != nil { + return nil, fmt.Errorf("OpenLazyReader: %w", err) + } + defer rd.Close() + } + // Get the two paths (might be the same if not moving) from the index if they exist filesInIndex, err := t.LsFiles(ctx, file.TreePath, file.FromTreePath) if err != nil { - return fmt.Errorf("UpdateRepoFile: %w", err) + return nil, fmt.Errorf("LsFiles: %w", err) } // If is a new file (not updating) then the given path shouldn't exist if file.Operation == "create" { if slices.Contains(filesInIndex, file.TreePath) { - return ErrRepoFileAlreadyExists{ - Path: file.TreePath, - } + return nil, ErrRepoFileAlreadyExists{Path: file.TreePath} } } @@ -474,7 +511,7 @@ func CreateUpdateRenameFile(ctx context.Context, t *TemporaryUploadRepository, f for _, indexFile := range filesInIndex { if indexFile == file.Options.fromTreePath { if err = t.RemoveFilesFromIndex(ctx, file.FromTreePath); err != nil { - return err + return nil, err } } } @@ -482,45 +519,46 @@ func CreateUpdateRenameFile(ctx context.Context, t *TemporaryUploadRepository, f var writeObjectRet *writeRepoObjectRet switch file.Operation { - case "create", "update": - writeObjectRet, err = writeRepoObjectForCreateOrUpdate(ctx, t, file) + case "create", "update", "upload": + writeObjectRet, err = writeRepoObjectForModify(ctx, t, file) case "rename": writeObjectRet, err = writeRepoObjectForRename(ctx, t, file) default: - return util.NewInvalidArgumentErrorf("unknown file modification operation: '%s'", file.Operation) + return nil, util.NewInvalidArgumentErrorf("unknown file modification operation: '%s'", file.Operation) } if err != nil { - return err + return nil, err } // Add the object to the index, the "file.Options.executable" is set in handleCheckErrors by the caller (legacy hacky approach) if err = t.AddObjectToIndex(ctx, util.Iif(file.Options.executable, "100755", "100644"), writeObjectRet.ObjectHash, file.Options.treePath); err != nil { - return err + return nil, err } if writeObjectRet.LfsContent == nil { - return nil // No LFS pointer, so nothing to do + return nil, nil // No LFS pointer, so nothing to do } defer writeObjectRet.LfsContent.Close() // Now we must store the content into an LFS object lfsMetaObject, err := git_model.NewLFSMetaObject(ctx, repoID, writeObjectRet.LfsPointer) if err != nil { - return err - } - if exist, err := contentStore.Exists(lfsMetaObject.Pointer); err != nil { - return err - } else if exist { - return nil + return nil, err } - - err = contentStore.Put(lfsMetaObject.Pointer, writeObjectRet.LfsContent) + exist, err := contentStore.Exists(lfsMetaObject.Pointer) if err != nil { - if _, errRemove := git_model.RemoveLFSMetaObjectByOid(ctx, repoID, lfsMetaObject.Oid); errRemove != nil { - return fmt.Errorf("unable to remove failed inserted LFS object %s: %v (Prev Error: %w)", lfsMetaObject.Oid, errRemove, err) + return nil, err + } + if !exist { + err = contentStore.Put(lfsMetaObject.Pointer, writeObjectRet.LfsContent) + if err != nil { + if _, errRemove := git_model.RemoveLFSMetaObjectByOid(ctx, repoID, lfsMetaObject.Oid); errRemove != nil { + return nil, fmt.Errorf("unable to remove failed inserted LFS object %s: %v (Prev Error: %w)", lfsMetaObject.Oid, errRemove, err) + } + return nil, err } } - return err + return &lfsMetaObject.Pointer, nil } func checkIsLfsFileInGitAttributes(ctx context.Context, t *TemporaryUploadRepository, paths []string) (ret []bool, err error) { @@ -544,8 +582,8 @@ type writeRepoObjectRet struct { LfsPointer lfs.Pointer } -// writeRepoObjectForCreateOrUpdate hashes the git object for create or update operations -func writeRepoObjectForCreateOrUpdate(ctx context.Context, t *TemporaryUploadRepository, file *ChangeRepoFile) (ret *writeRepoObjectRet, err error) { +// writeRepoObjectForModify hashes the git object for create or update operations +func writeRepoObjectForModify(ctx context.Context, t *TemporaryUploadRepository, file *ChangeRepoFile) (ret *writeRepoObjectRet, err error) { ret = &writeRepoObjectRet{} treeObjectContentReader := file.ContentReader if setting.LFS.StartServer { @@ -574,7 +612,7 @@ func writeRepoObjectForCreateOrUpdate(ctx context.Context, t *TemporaryUploadRep return ret, nil } -// writeRepoObjectForRename the same as writeRepoObjectForCreateOrUpdate buf for "rename" +// writeRepoObjectForRename the same as writeRepoObjectForModify buf for "rename" func writeRepoObjectForRename(ctx context.Context, t *TemporaryUploadRepository, file *ChangeRepoFile) (ret *writeRepoObjectRet, err error) { lastCommitID, err := t.GetLastCommit(ctx) if err != nil { diff --git a/services/repository/files/upload.go b/services/repository/files/upload.go index b004e3cc4c..b783cbd01d 100644 --- a/services/repository/files/upload.go +++ b/services/repository/files/upload.go @@ -8,15 +8,11 @@ import ( "fmt" "os" "path" - "strings" + "sync" - git_model "code.gitea.io/gitea/models/git" repo_model "code.gitea.io/gitea/models/repo" user_model "code.gitea.io/gitea/models/user" - "code.gitea.io/gitea/modules/git" - "code.gitea.io/gitea/modules/git/attribute" - "code.gitea.io/gitea/modules/lfs" - "code.gitea.io/gitea/modules/setting" + "code.gitea.io/gitea/modules/log" ) // UploadRepoFileOptions contains the uploaded repository file options @@ -32,208 +28,84 @@ type UploadRepoFileOptions struct { Committer *IdentityOptions } -type uploadInfo struct { - upload *repo_model.Upload - lfsMetaObject *git_model.LFSMetaObject +type lazyLocalFileReader struct { + *os.File + localFilename string + counter int + mu sync.Mutex } -func cleanUpAfterFailure(ctx context.Context, infos *[]uploadInfo, t *TemporaryUploadRepository, original error) error { - for _, info := range *infos { - if info.lfsMetaObject == nil { - continue - } - if !info.lfsMetaObject.Existing { - if _, err := git_model.RemoveLFSMetaObjectByOid(ctx, t.repo.ID, info.lfsMetaObject.Oid); err != nil { - original = fmt.Errorf("%w, %v", original, err) // We wrap the original error - as this is the underlying error that required the fallback - } - } - } - return original -} - -// UploadRepoFiles uploads files to the given repository -func UploadRepoFiles(ctx context.Context, repo *repo_model.Repository, doer *user_model.User, opts *UploadRepoFileOptions) error { - if len(opts.Files) == 0 { - return nil - } +var _ LazyReadSeeker = (*lazyLocalFileReader)(nil) - uploads, err := repo_model.GetUploadsByUUIDs(ctx, opts.Files) - if err != nil { - return fmt.Errorf("GetUploadsByUUIDs [uuids: %v]: %w", opts.Files, err) - } +func (l *lazyLocalFileReader) Close() error { + l.mu.Lock() + defer l.mu.Unlock() - names := make([]string, len(uploads)) - infos := make([]uploadInfo, len(uploads)) - for i, upload := range uploads { - // Check file is not lfs locked, will return nil if lock setting not enabled - filepath := path.Join(opts.TreePath, upload.Name) - lfsLock, err := git_model.GetTreePathLock(ctx, repo.ID, filepath) - if err != nil { - return err - } - if lfsLock != nil && lfsLock.OwnerID != doer.ID { - u, err := user_model.GetUserByID(ctx, lfsLock.OwnerID) - if err != nil { - return err + if l.counter > 0 { + l.counter-- + if l.counter == 0 { + if err := l.File.Close(); err != nil { + return fmt.Errorf("close file %s: %w", l.localFilename, err) } - return git_model.ErrLFSFileLocked{RepoID: repo.ID, Path: filepath, UserName: u.Name} - } - - names[i] = upload.Name - infos[i] = uploadInfo{upload: upload} - } - - t, err := NewTemporaryUploadRepository(repo) - if err != nil { - return err - } - defer t.Close() - - hasOldBranch := true - if err = t.Clone(ctx, opts.OldBranch, true); err != nil { - if !git.IsErrBranchNotExist(err) || !repo.IsEmpty { - return err - } - if err = t.Init(ctx, repo.ObjectFormatName); err != nil { - return err - } - hasOldBranch = false - opts.LastCommitID = "" - } - if hasOldBranch { - if err = t.SetDefaultIndex(ctx); err != nil { - return err - } - } - - var attributesMap map[string]*attribute.Attributes - // when uploading to an empty repo, the old branch doesn't exist, but some "global gitattributes" or "info/attributes" may exist - if setting.LFS.StartServer { - attributesMap, err = attribute.CheckAttributes(ctx, t.gitRepo, "" /* use temp repo's working dir */, attribute.CheckAttributeOpts{ - Attributes: []string{attribute.Filter}, - Filenames: names, - }) - if err != nil { - return err + l.File = nil } + return nil } + return fmt.Errorf("file %s already closed", l.localFilename) +} - // Copy uploaded files into repository. - // TODO: there is a small problem: when uploading LFS files with ".gitattributes", the "check-attr" runs before this loop, - // so LFS files are not able to be added as LFS objects. Ideally we need to do in 3 steps in the future: - // 1. Add ".gitattributes" to git index - // 2. Run "check-attr" (the previous attribute.CheckAttributes call) - // 3. Add files to git index (this loop) - // This problem is trivial so maybe no need to spend too much time on it at the moment. - for i := range infos { - if err := copyUploadedLFSFileIntoRepository(ctx, &infos[i], attributesMap, t, opts.TreePath); err != nil { - return err - } - } +func (l *lazyLocalFileReader) OpenLazyReader() error { + l.mu.Lock() + defer l.mu.Unlock() - // Now write the tree - treeHash, err := t.WriteTree(ctx) - if err != nil { - return err + if l.File != nil { + l.counter++ + return nil } - // Now commit the tree - commitOpts := &CommitTreeUserOptions{ - ParentCommitID: opts.LastCommitID, - TreeHash: treeHash, - CommitMessage: opts.Message, - SignOff: opts.Signoff, - DoerUser: doer, - AuthorIdentity: opts.Author, - CommitterIdentity: opts.Committer, - } - commitHash, err := t.CommitTree(ctx, commitOpts) + file, err := os.Open(l.localFilename) if err != nil { return err } + l.File = file + l.counter = 1 + return nil +} - // Now deal with LFS objects - for i := range infos { - if infos[i].lfsMetaObject == nil { - continue - } - infos[i].lfsMetaObject, err = git_model.NewLFSMetaObject(ctx, infos[i].lfsMetaObject.RepositoryID, infos[i].lfsMetaObject.Pointer) - if err != nil { - // OK Now we need to cleanup - return cleanUpAfterFailure(ctx, &infos, t, err) - } - // Don't move the files yet - we need to ensure that - // everything can be inserted first - } - - // OK now we can insert the data into the store - there's no way to clean up the store - // once it's in there, it's in there. - contentStore := lfs.NewContentStore() - for _, info := range infos { - if err := uploadToLFSContentStore(info, contentStore); err != nil { - return cleanUpAfterFailure(ctx, &infos, t, err) - } - } - - // Then push this tree to NewBranch - if err := t.Push(ctx, doer, commitHash, opts.NewBranch); err != nil { - return err +// UploadRepoFiles uploads files to the given repository +func UploadRepoFiles(ctx context.Context, repo *repo_model.Repository, doer *user_model.User, opts *UploadRepoFileOptions) error { + if len(opts.Files) == 0 { + return nil } - return repo_model.DeleteUploads(ctx, uploads...) -} - -func copyUploadedLFSFileIntoRepository(ctx context.Context, info *uploadInfo, attributesMap map[string]*attribute.Attributes, t *TemporaryUploadRepository, treePath string) error { - file, err := os.Open(info.upload.LocalPath()) + uploads, err := repo_model.GetUploadsByUUIDs(ctx, opts.Files) if err != nil { - return err + return fmt.Errorf("GetUploadsByUUIDs [uuids: %v]: %w", opts.Files, err) } - defer file.Close() - var objectHash string - if setting.LFS.StartServer && attributesMap[info.upload.Name] != nil && attributesMap[info.upload.Name].Get(attribute.Filter).ToString().Value() == "lfs" { - // Handle LFS - // FIXME: Inefficient! this should probably happen in models.Upload - pointer, err := lfs.GeneratePointer(file) - if err != nil { - return err - } - - info.lfsMetaObject = &git_model.LFSMetaObject{Pointer: pointer, RepositoryID: t.repo.ID} - - if objectHash, err = t.HashObjectAndWrite(ctx, strings.NewReader(pointer.StringContent())); err != nil { - return err - } - } else if objectHash, err = t.HashObjectAndWrite(ctx, file); err != nil { - return err + changeOpts := &ChangeRepoFilesOptions{ + LastCommitID: opts.LastCommitID, + OldBranch: opts.OldBranch, + NewBranch: opts.NewBranch, + Message: opts.Message, + Signoff: opts.Signoff, + Author: opts.Author, + Committer: opts.Committer, + } + for _, upload := range uploads { + changeOpts.Files = append(changeOpts.Files, &ChangeRepoFile{ + Operation: "upload", + TreePath: path.Join(opts.TreePath, upload.Name), + ContentReader: &lazyLocalFileReader{localFilename: upload.LocalPath()}, + }) } - // Add the object to the index - return t.AddObjectToIndex(ctx, "100644", objectHash, path.Join(treePath, info.upload.Name)) -} - -func uploadToLFSContentStore(info uploadInfo, contentStore *lfs.ContentStore) error { - if info.lfsMetaObject == nil { - return nil - } - exist, err := contentStore.Exists(info.lfsMetaObject.Pointer) + _, err = ChangeRepoFiles(ctx, repo, doer, changeOpts) if err != nil { return err } - if !exist { - file, err := os.Open(info.upload.LocalPath()) - if err != nil { - return err - } - - defer file.Close() - // FIXME: Put regenerates the hash and copies the file over. - // I guess this strictly ensures the soundness of the store but this is inefficient. - if err := contentStore.Put(info.lfsMetaObject.Pointer, file); err != nil { - // OK Now we need to cleanup - // Can't clean up the store, once uploaded there they're there. - return err - } + if err := repo_model.DeleteUploads(ctx, uploads...); err != nil { + log.Error("DeleteUploads: %v", err) } return nil } diff --git a/services/webhook/telegram.go b/services/webhook/telegram.go index ae195758b9..fdd428b45c 100644 --- a/services/webhook/telegram.go +++ b/services/webhook/telegram.go @@ -195,7 +195,7 @@ func (telegramConvertor) WorkflowJob(p *api.WorkflowJobPayload) (TelegramPayload func createTelegramPayloadHTML(msgHTML string) TelegramPayload { // https://core.telegram.org/bots/api#formatting-options return TelegramPayload{ - Message: strings.TrimSpace(markup.Sanitize(msgHTML)), + Message: strings.TrimSpace(string(markup.Sanitize(msgHTML))), ParseMode: "HTML", DisableWebPreview: true, } diff --git a/templates/admin/packages/list.tmpl b/templates/admin/packages/list.tmpl index 0c6889b599..985caf6bdf 100644 --- a/templates/admin/packages/list.tmpl +++ b/templates/admin/packages/list.tmpl @@ -90,7 +90,7 @@ {{ctx.Locale.Tr "packages.settings.delete"}} </div> <div class="content"> - {{ctx.Locale.Tr "packages.settings.delete.notice" (`<span class="name"></span>`|SafeHTML) (`<span class="dataVersion"></span>`|SafeHTML)}} + {{ctx.Locale.Tr "packages.settings.delete.notice" (HTMLFormat `<span class="%s"></span>` "name") (HTMLFormat `<span class="%s"></span>` "dataVersion")}} </div> {{template "base/modal_actions_confirm" .}} </div> diff --git a/templates/admin/repo/list.tmpl b/templates/admin/repo/list.tmpl index 762013af47..af80633be0 100644 --- a/templates/admin/repo/list.tmpl +++ b/templates/admin/repo/list.tmpl @@ -103,7 +103,7 @@ </div> <div class="content"> <p>{{ctx.Locale.Tr "repo.settings.delete_desc"}}</p> - {{ctx.Locale.Tr "repo.settings.delete_notices_2" (`<span class="name"></span>`|SafeHTML)}}<br> + {{ctx.Locale.Tr "repo.settings.delete_notices_2" (HTMLFormat `<span class="%s"></span>` "name")}}<br> {{ctx.Locale.Tr "repo.settings.delete_notices_fork_1"}}<br> </div> {{template "base/modal_actions_confirm" .}} diff --git a/templates/admin/user/view.tmpl b/templates/admin/user/view.tmpl index 31616ffbf9..67f9148e64 100644 --- a/templates/admin/user/view.tmpl +++ b/templates/admin/user/view.tmpl @@ -26,7 +26,7 @@ {{ctx.Locale.Tr "admin.repositories"}} ({{ctx.Locale.Tr "admin.total" .ReposTotal}}) </h4> <div class="ui attached segment"> - {{template "explore/repo_list" .}} + {{template "shared/repo/list" .}} </div> <h4 class="ui top attached header"> {{ctx.Locale.Tr "settings.organization"}} ({{ctx.Locale.Tr "admin.total" .OrgsTotal}}) diff --git a/templates/explore/repos.tmpl b/templates/explore/repos.tmpl index 53742bf0d9..68da398306 100644 --- a/templates/explore/repos.tmpl +++ b/templates/explore/repos.tmpl @@ -2,8 +2,8 @@ <div role="main" aria-label="{{.Title}}" class="page-content explore repositories"> {{template "explore/navbar" .}} <div class="ui container"> - {{template "shared/repo_search" .}} - {{template "explore/repo_list" .}} + {{template "shared/repo/search" .}} + {{template "shared/repo/list" .}} {{template "base/paginate" .}} </div> </div> diff --git a/templates/org/home.tmpl b/templates/org/home.tmpl index cffdfabfaa..3cde3554c9 100644 --- a/templates/org/home.tmpl +++ b/templates/org/home.tmpl @@ -8,8 +8,8 @@ {{if .ProfileReadmeContent}} <div id="readme_profile" class="render-content markup" data-profile-view-as-member="{{.IsViewingOrgAsMember}}">{{.ProfileReadmeContent}}</div> {{end}} - {{template "shared/repo_search" .}} - {{template "explore/repo_list" .}} + {{template "shared/repo/search" .}} + {{template "shared/repo/list" .}} {{template "base/paginate" .}} </div> diff --git a/templates/org/member/members.tmpl b/templates/org/member/members.tmpl index 4388dc9520..2d0f4bc423 100644 --- a/templates/org/member/members.tmpl +++ b/templates/org/member/members.tmpl @@ -73,7 +73,7 @@ {{ctx.Locale.Tr "org.members.leave"}} </div> <div class="content"> - <p>{{ctx.Locale.Tr "org.members.leave.detail" (`<span class="dataOrganizationName"></span>`|SafeHTML)}}</p> + <p>{{ctx.Locale.Tr "org.members.leave.detail" (HTMLFormat `<span class="%s"></span>` "dataOrganizationName")}}</p> </div> {{template "base/modal_actions_confirm" .}} </div> @@ -82,7 +82,7 @@ {{ctx.Locale.Tr "org.members.remove"}} </div> <div class="content"> - <p>{{ctx.Locale.Tr "org.members.remove.detail" (`<span class="name"></span>`|SafeHTML) (`<span class="dataOrganizationName"></span>`|SafeHTML)}}</p> + <p>{{ctx.Locale.Tr "org.members.remove.detail" (HTMLFormat `<span class="%s"></span>` "name") (HTMLFormat `<span class="%s"></span>` "dataOrganizationName")}}</p> </div> {{template "base/modal_actions_confirm" .}} </div> diff --git a/templates/org/settings/delete.tmpl b/templates/org/settings/delete.tmpl deleted file mode 100644 index e1ef471e34..0000000000 --- a/templates/org/settings/delete.tmpl +++ /dev/null @@ -1,35 +0,0 @@ -{{template "org/settings/layout_head" (dict "ctxData" . "pageClass" "organization settings delete")}} - - <div class="org-setting-content"> - <h4 class="ui top attached error header"> - {{ctx.Locale.Tr "org.settings.delete_account"}} - </h4> - <div class="ui attached error segment"> - <div class="ui red message"> - <p class="text left">{{svg "octicon-alert"}} {{ctx.Locale.Tr "org.settings.delete_prompt"}}</p> - </div> - <form class="ui form ignore-dirty" id="delete-form" action="{{.Link}}" method="post"> - {{.CsrfTokenHtml}} - <div class="inline required field {{if .Err_OrgName}}error{{end}}"> - <label for="org_name">{{ctx.Locale.Tr "org.org_name_holder"}}</label> - <input id="org_name" name="org_name" value="" autocomplete="off" autofocus required> - </div> - <button class="ui red button delete-button" data-type="form" data-form="#delete-form"> - {{ctx.Locale.Tr "org.settings.confirm_delete_account"}} - </button> - </form> - </div> - </div> - -<div class="ui g-modal-confirm delete modal"> - <div class="header"> - {{svg "octicon-trash"}} - {{ctx.Locale.Tr "org.settings.delete_org_title"}} - </div> - <div class="content"> - <p>{{ctx.Locale.Tr "org.settings.delete_org_desc"}}</p> - </div> - {{template "base/modal_actions_confirm" .}} -</div> - -{{template "org/settings/layout_footer" .}} diff --git a/templates/org/settings/navbar.tmpl b/templates/org/settings/navbar.tmpl index ce792f667c..58475de7e7 100644 --- a/templates/org/settings/navbar.tmpl +++ b/templates/org/settings/navbar.tmpl @@ -41,8 +41,5 @@ </div> </details> {{end}} - <a class="{{if .PageIsSettingsDelete}}active {{end}}item" href="{{.OrgLink}}/settings/delete"> - {{ctx.Locale.Tr "org.settings.delete"}} - </a> </div> </div> diff --git a/templates/org/settings/options.tmpl b/templates/org/settings/options.tmpl index f4583bbe36..d94bb4c62b 100644 --- a/templates/org/settings/options.tmpl +++ b/templates/org/settings/options.tmpl @@ -1,101 +1,97 @@ {{template "org/settings/layout_head" (dict "ctxData" . "pageClass" "organization settings options")}} - <div class="org-setting-content"> - <h4 class="ui top attached header"> - {{ctx.Locale.Tr "org.settings.options"}} - </h4> - <div class="ui attached segment"> - <form class="ui form" action="{{.Link}}" method="post"> - {{.CsrfTokenHtml}} - <div class="required field {{if .Err_Name}}error{{end}}"> - <label for="org_name">{{ctx.Locale.Tr "org.org_name_holder"}} - <span class="text red tw-hidden" id="org-name-change-prompt"> - <br>{{ctx.Locale.Tr "org.settings.change_orgname_prompt"}}<br>{{ctx.Locale.Tr "org.settings.change_orgname_redirect_prompt"}} - </span> - </label> - <input id="org_name" name="name" value="{{.Org.Name}}" data-org-name="{{.Org.Name}}" required maxlength="40"> - </div> - <div class="field {{if .Err_FullName}}error{{end}}"> - <label for="full_name">{{ctx.Locale.Tr "org.org_full_name_holder"}}</label> - <input id="full_name" name="full_name" value="{{.Org.FullName}}" maxlength="100"> - </div> - <div class="field {{if .Err_Email}}error{{end}}"> - <label for="email">{{ctx.Locale.Tr "org.settings.email"}}</label> - <input id="email" name="email" type="email" value="{{.Org.Email}}" maxlength="255"> - </div> - <div class="field {{if .Err_Description}}error{{end}}"> - {{/* it is rendered as markdown, but the length is limited, so at the moment we do not use the markdown editor here */}} - <label for="description">{{ctx.Locale.Tr "org.org_desc"}}</label> - <textarea id="description" name="description" rows="2" maxlength="255">{{.Org.Description}}</textarea> - </div> - <div class="field {{if .Err_Website}}error{{end}}"> - <label for="website">{{ctx.Locale.Tr "org.settings.website"}}</label> - <input id="website" name="website" type="url" value="{{.Org.Website}}" maxlength="255"> - </div> - <div class="field"> - <label for="location">{{ctx.Locale.Tr "org.settings.location"}}</label> - <input id="location" name="location" value="{{.Org.Location}}" maxlength="50"> - </div> - <div class="divider"></div> - <div class="field" id="visibility_box"> - <label for="visibility">{{ctx.Locale.Tr "org.settings.visibility"}}</label> - <div class="field"> - <div class="ui radio checkbox"> - <input class="enable-system-radio" name="visibility" type="radio" value="0" {{if eq .CurrentVisibility 0}}checked{{end}}> - <label>{{ctx.Locale.Tr "org.settings.visibility.public"}}</label> - </div> - </div> - <div class="field"> - <div class="ui radio checkbox"> - <input class="enable-system-radio" name="visibility" type="radio" value="1" {{if eq .CurrentVisibility 1}}checked{{end}}> - <label>{{ctx.Locale.Tr "org.settings.visibility.limited"}}</label> - </div> - </div> - <div class="field"> - <div class="ui radio checkbox"> - <input class="enable-system-radio" name="visibility" type="radio" value="2" {{if eq .CurrentVisibility 2}}checked{{end}}> - <label>{{ctx.Locale.Tr "org.settings.visibility.private"}}</label> - </div> - </div> - </div> +<div class="ui segments org-setting-content"> + <h4 class="ui top attached header"> + {{ctx.Locale.Tr "org.settings.options"}} + </h4> + <div class="ui attached segment"> + <form class="ui form" action="{{.Link}}" method="post"> + {{.CsrfTokenHtml}} + <div class="field {{if .Err_FullName}}error{{end}}"> + <label for="full_name">{{ctx.Locale.Tr "org.org_full_name_holder"}}</label> + <input id="full_name" name="full_name" value="{{.Org.FullName}}" maxlength="100"> + </div> + <div class="field {{if .Err_Email}}error{{end}}"> + <label for="email">{{ctx.Locale.Tr "org.settings.email"}}</label> + <input id="email" name="email" type="email" value="{{.Org.Email}}" maxlength="255"> + </div> + <div class="field {{if .Err_Description}}error{{end}}"> + {{/* it is rendered as markdown, but the length is limited, so at the moment we do not use the markdown editor here */}} + <label for="description">{{ctx.Locale.Tr "org.org_desc"}}</label> + <textarea id="description" name="description" rows="2" maxlength="255">{{.Org.Description}}</textarea> + </div> + <div class="field {{if .Err_Website}}error{{end}}"> + <label for="website">{{ctx.Locale.Tr "org.settings.website"}}</label> + <input id="website" name="website" type="url" value="{{.Org.Website}}" maxlength="255"> + </div> + <div class="field"> + <label for="location">{{ctx.Locale.Tr "org.settings.location"}}</label> + <input id="location" name="location" value="{{.Org.Location}}" maxlength="50"> + </div> + + <div class="divider"></div> + <div class="field" id="visibility_box"> + <label for="visibility">{{ctx.Locale.Tr "org.settings.visibility"}}</label> + <div class="field"> + <div class="ui radio checkbox"> + <input class="enable-system-radio" name="visibility" type="radio" value="0" {{if eq .CurrentVisibility 0}}checked{{end}}> + <label>{{ctx.Locale.Tr "org.settings.visibility.public"}}</label> + </div> + </div> + <div class="field"> + <div class="ui radio checkbox"> + <input class="enable-system-radio" name="visibility" type="radio" value="1" {{if eq .CurrentVisibility 1}}checked{{end}}> + <label>{{ctx.Locale.Tr "org.settings.visibility.limited"}}</label> + </div> + </div> + <div class="field"> + <div class="ui radio checkbox"> + <input class="enable-system-radio" name="visibility" type="radio" value="2" {{if eq .CurrentVisibility 2}}checked{{end}}> + <label>{{ctx.Locale.Tr "org.settings.visibility.private"}}</label> + </div> + </div> + </div> - <div class="field" id="permission_box"> - <label>{{ctx.Locale.Tr "org.settings.permission"}}</label> - <div class="field"> - <div class="ui checkbox"> - <input type="checkbox" name="repo_admin_change_team_access" {{if .RepoAdminChangeTeamAccess}}checked{{end}}> - <label>{{ctx.Locale.Tr "org.settings.repoadminchangeteam"}}</label> - </div> - </div> - </div> + <div class="field" id="permission_box"> + <label>{{ctx.Locale.Tr "org.settings.permission"}}</label> + <div class="field"> + <div class="ui checkbox"> + <input type="checkbox" name="repo_admin_change_team_access" {{if .RepoAdminChangeTeamAccess}}checked{{end}}> + <label>{{ctx.Locale.Tr "org.settings.repoadminchangeteam"}}</label> + </div> + </div> + </div> - {{if .SignedUser.IsAdmin}} - <div class="divider"></div> + {{if .SignedUser.IsAdmin}} + <div class="divider"></div> - <div class="inline field {{if .Err_MaxRepoCreation}}error{{end}}"> - <label for="max_repo_creation">{{ctx.Locale.Tr "admin.users.max_repo_creation"}}</label> - <input id="max_repo_creation" name="max_repo_creation" type="number" min="-1" value="{{.Org.MaxRepoCreation}}"> - <p class="help">{{ctx.Locale.Tr "admin.users.max_repo_creation_desc"}}</p> - </div> - {{end}} + <div class="inline field {{if .Err_MaxRepoCreation}}error{{end}}"> + <label for="max_repo_creation">{{ctx.Locale.Tr "admin.users.max_repo_creation"}}</label> + <input id="max_repo_creation" name="max_repo_creation" type="number" min="-1" value="{{.Org.MaxRepoCreation}}"> + <p class="help">{{ctx.Locale.Tr "admin.users.max_repo_creation_desc"}}</p> + </div> + {{end}} - <div class="field"> - <button class="ui primary button">{{ctx.Locale.Tr "org.settings.update_settings"}}</button> - </div> - </form> + <div class="field"> + <button class="ui primary button">{{ctx.Locale.Tr "org.settings.update_settings"}}</button> + </div> + </form> - <div class="divider"></div> + <div class="divider"></div> - <form class="ui form" action="{{.Link}}/avatar" method="post" enctype="multipart/form-data"> - {{.CsrfTokenHtml}} - <div class="inline field"> - {{template "shared/avatar_upload_crop" dict "LabelText" (ctx.Locale.Tr "settings.choose_new_avatar")}} - </div> - <div class="field"> - <button class="ui primary button">{{ctx.Locale.Tr "settings.update_avatar"}}</button> - <button class="ui red button link-action" data-url="{{.Link}}/avatar/delete">{{ctx.Locale.Tr "settings.delete_current_avatar"}}</button> - </div> - </form> - </div> + <form class="ui form" action="{{.Link}}/avatar" method="post" enctype="multipart/form-data"> + {{.CsrfTokenHtml}} + <div class="inline field"> + {{template "shared/avatar_upload_crop" dict "LabelText" (ctx.Locale.Tr "settings.choose_new_avatar")}} + </div> + <div class="field"> + <button class="ui primary button">{{ctx.Locale.Tr "settings.update_avatar"}}</button> + <button class="ui red button link-action" data-url="{{.Link}}/avatar/delete">{{ctx.Locale.Tr "settings.delete_current_avatar"}}</button> </div> + </form> + </div> +</div> + +{{template "org/settings/options_dangerzone" .}} + {{template "org/settings/layout_footer" .}} diff --git a/templates/org/settings/options_dangerzone.tmpl b/templates/org/settings/options_dangerzone.tmpl new file mode 100644 index 0000000000..01cf3fd405 --- /dev/null +++ b/templates/org/settings/options_dangerzone.tmpl @@ -0,0 +1,93 @@ +<h4 class="ui top attached error header"> + {{ctx.Locale.Tr "repo.settings.danger_zone"}} +</h4> +<div class="ui attached error danger segment"> + <div class="flex-list"> + <div class="flex-item tw-items-center"> + <div class="flex-item-main"> + <div class="flex-item-title">{{ctx.Locale.Tr "org.settings.rename"}}</div> + <div class="flex-item-body">{{ctx.Locale.Tr "org.settings.rename_desc"}}</div> + </div> + <div class="flex-item-trailing"> + <button class="ui basic red show-modal button" data-modal="#rename-org-modal">{{ctx.Locale.Tr "org.settings.rename"}}</button> + </div> + </div> + + <div class="flex-item"> + <div class="flex-item-main"> + <div class="flex-item-title">{{ctx.Locale.Tr "org.settings.delete_account"}}</div> + <div class="flex-item-body">{{ctx.Locale.Tr "org.settings.delete_prompt"}}</div> + </div> + <div class="flex-item-trailing"> + <button class="ui basic red show-modal button" data-modal="#delete-org-modal">{{ctx.Locale.Tr "org.settings.delete_account"}}</button> + </div> + </div> + </div> +</div> + +<div class="ui small modal" id="rename-org-modal"> + <div class="header"> + {{ctx.Locale.Tr "org.settings.rename"}} + </div> + <div class="content"> + <ul class="ui warning message"> + <li>{{ctx.Locale.Tr "org.settings.rename_notices_1"}}</li> + <li>{{ctx.Locale.Tr "org.settings.rename_notices_2"}}</li> + </ul> + <form class="ui form form-fetch-action" action="{{.Link}}/rename" method="post"> + {{.CsrfTokenHtml}} + <div class="field"> + <label> + {{ctx.Locale.Tr "org.settings.name_confirm"}} + <span class="text red">{{.Org.Name}}</span> + </label> + </div> + <div class="required field"> + <label for="org_name_to_rename">{{ctx.Locale.Tr "org.org_name_holder"}}</label> + <input id="org_name_to_rename" name="org_name" required> + </div> + + <div class="required field"> + <label>{{ctx.Locale.Tr "org.settings.rename_new_org_name"}}</label> + <input name="new_org_name" required> + </div> + + <div class="actions"> + <button class="ui cancel button">{{ctx.Locale.Tr "settings.cancel"}}</button> + <button class="ui red button">{{ctx.Locale.Tr "org.settings.rename"}}</button> + </div> + </form> + </div> +</div> + +<div class="ui small modal" id="delete-org-modal"> + <div class="header"> + {{ctx.Locale.Tr "org.settings.delete_account"}} + </div> + <div class="content"> + <ul class="ui warning message"> + <li>{{ctx.Locale.Tr "org.settings.delete_notices_1"}}</li> + <li>{{ctx.Locale.Tr "org.settings.delete_notices_2" .Org.Name}}</li> + <li>{{ctx.Locale.Tr "org.settings.delete_notices_3" .Org.Name}}</li> + <li>{{ctx.Locale.Tr "org.settings.delete_notices_4" .Org.Name}}</li> + </ul> + <form class="ui form form-fetch-action" action="{{.Link}}/delete" method="post"> + {{.CsrfTokenHtml}} + <div class="field"> + <label> + {{ctx.Locale.Tr "org.settings.name_confirm"}} + <span class="text red">{{.Org.Name}}</span> + </label> + </div> + <div class="required field"> + <label>{{ctx.Locale.Tr "org.org_name_holder"}}</label> + <input name="org_name" required> + </div> + + <div class="actions"> + <button class="ui cancel button">{{ctx.Locale.Tr "settings.cancel"}}</button> + <button class="ui red button">{{ctx.Locale.Tr "org.settings.delete_account"}}</button> + </div> + </form> + </div> +</div> diff --git a/templates/org/team/members.tmpl b/templates/org/team/members.tmpl index 5433f01530..4bc063f90c 100644 --- a/templates/org/team/members.tmpl +++ b/templates/org/team/members.tmpl @@ -81,7 +81,7 @@ {{ctx.Locale.Tr "org.members.remove"}} </div> <div class="content"> - <p>{{ctx.Locale.Tr "org.members.remove.detail" (`<span class="name"></span>`|SafeHTML) (`<span class="dataTeamName"></span>`|SafeHTML)}}</p> + <p>{{ctx.Locale.Tr "org.members.remove.detail" (HTMLFormat `<span class="%s"></span>` "name") (HTMLFormat `<span class="%s"></span>` "dataTeamName")}}</p> </div> {{template "base/modal_actions_confirm" .}} </div> diff --git a/templates/org/team/sidebar.tmpl b/templates/org/team/sidebar.tmpl index 8390bf0acd..6dd5cb3eeb 100644 --- a/templates/org/team/sidebar.tmpl +++ b/templates/org/team/sidebar.tmpl @@ -90,7 +90,7 @@ {{ctx.Locale.Tr "org.teams.leave"}} </div> <div class="content"> - <p>{{ctx.Locale.Tr "org.teams.leave.detail" (`<span class="name"></span>`|SafeHTML)}}</p> + <p>{{ctx.Locale.Tr "org.teams.leave.detail" (HTMLFormat `<span class="%s"></span>` "name")}}</p> </div> {{template "base/modal_actions_confirm" .}} </div> diff --git a/templates/org/team/teams.tmpl b/templates/org/team/teams.tmpl index 432df10749..cdd2789128 100644 --- a/templates/org/team/teams.tmpl +++ b/templates/org/team/teams.tmpl @@ -49,7 +49,7 @@ {{ctx.Locale.Tr "org.teams.leave"}} </div> <div class="content"> - <p>{{ctx.Locale.Tr "org.teams.leave.detail" (`<span class="name"></span>`|SafeHTML)}}</p> + <p>{{ctx.Locale.Tr "org.teams.leave.detail" (HTMLFormat `<span class="%s"></span>` "name")}}</p> </div> {{template "base/modal_actions_confirm" .}} </div> diff --git a/templates/post-install.tmpl b/templates/post-install.tmpl index 0c9aa35c90..9baac4f84c 100644 --- a/templates/post-install.tmpl +++ b/templates/post-install.tmpl @@ -4,7 +4,7 @@ <!-- the "cup" has a handler, so move it a little leftward to make it visually in the center --> <div class="tw-ml-[-30px]"><img width="160" src="{{AssetUrlPrefix}}/img/loading.png" alt aria-hidden="true"></div> <div class="tw-my-[2em] tw-text-[18px]"> - <a id="goto-user-login" href="{{AppSubUrl}}/user/login">{{ctx.Locale.Tr "install.installing_desc"}}</a> + <a id="goto-after-install" href="{{AppSubUrl}}{{Iif .IsAccountCreated "/user/login" "/user/sign_up"}}">{{ctx.Locale.Tr "install.installing_desc"}}</a> </div> </div> </div> diff --git a/templates/repo/actions/runs_list.tmpl b/templates/repo/actions/runs_list.tmpl index fdb631f0ee..23df61a43c 100644 --- a/templates/repo/actions/runs_list.tmpl +++ b/templates/repo/actions/runs_list.tmpl @@ -40,7 +40,12 @@ {{svg "octicon-kebab-horizontal"}} <div class="menu flex-items-menu"> <a class="item" href="{{$run.Link}}/workflow">{{svg "octicon-play"}}{{ctx.Locale.Tr "actions.runs.view_workflow_file"}}</a> - {{if and $.AllowDeleteWorkflowRuns $run.Status.IsDone}} + {{if and $.CanWriteRepoUnitActions (not $run.Status.IsDone)}} + <a class="item link-action" data-url="{{$run.Link}}/cancel"> + {{svg "octicon-x"}}{{ctx.Locale.Tr "actions.runs.cancel"}} + </a> + {{end}} + {{if and $.CanWriteRepoUnitActions $run.Status.IsDone}} <a class="item link-action" data-url="{{$run.Link}}/delete" data-modal-confirm="{{ctx.Locale.Tr "actions.runs.delete.description"}}" diff --git a/templates/repo/actions/view_component.tmpl b/templates/repo/actions/view_component.tmpl index d0741cdc0b..4e338ffcfc 100644 --- a/templates/repo/actions/view_component.tmpl +++ b/templates/repo/actions/view_component.tmpl @@ -4,7 +4,7 @@ data-actions-url="{{.ActionsURL}}" data-locale-approve="{{ctx.Locale.Tr "repo.diff.review.approve"}}" - data-locale-cancel="{{ctx.Locale.Tr "cancel"}}" + data-locale-cancel="{{ctx.Locale.Tr "actions.runs.cancel"}}" data-locale-rerun="{{ctx.Locale.Tr "rerun"}}" data-locale-rerun-all="{{ctx.Locale.Tr "rerun_all"}}" data-locale-runs-scheduled="{{ctx.Locale.Tr "actions.runs.scheduled"}}" diff --git a/templates/repo/commit_page.tmpl b/templates/repo/commit_page.tmpl index fb0a63eff7..46f641824b 100644 --- a/templates/repo/commit_page.tmpl +++ b/templates/repo/commit_page.tmpl @@ -75,7 +75,7 @@ {{.CsrfTokenHtml}} <div class="field"> <label> - {{ctx.Locale.Tr "repo.branch.new_branch_from" (`<span class="text" id="modal-create-branch-from-span"></span>`|SafeHTML)}} + {{ctx.Locale.Tr "repo.branch.new_branch_from" (HTMLFormat `<span class="%s" id="%s"></span>` "text" "modal-create-branch-from-span")}} </label> </div> <div class="required field"> @@ -100,7 +100,7 @@ <input type="hidden" name="create_tag" value="true"> <div class="field"> <label> - {{ctx.Locale.Tr "repo.tag.create_tag_from" (`<span class="text" id="modal-create-tag-from-span"></span>`|SafeHTML)}} + {{ctx.Locale.Tr "repo.tag.create_tag_from" (HTMLFormat `<span class="%s" id="%s"></span>` "text" "modal-create-tag-from-span")}} </label> </div> <div class="required field"> diff --git a/templates/repo/editor/cherry_pick.tmpl b/templates/repo/editor/cherry_pick.tmpl index f9c9eef5aa..7981fd0761 100644 --- a/templates/repo/editor/cherry_pick.tmpl +++ b/templates/repo/editor/cherry_pick.tmpl @@ -3,15 +3,14 @@ {{template "repo/header" .}} <div class="ui container"> {{template "base/alert" .}} - <form class="ui edit form" method="post" action="{{.RepoLink}}/_cherrypick/{{.SHA}}/{{.BranchName | PathEscapeSegments}}"> + <form class="ui edit form form-fetch-action" method="post" action="{{.CommitFormOptions.TargetFormAction}}"> {{.CsrfTokenHtml}} - <input type="hidden" name="last_commit" value="{{.last_commit}}"> - <input type="hidden" name="page_has_posted" value="true"> + {{template "repo/editor/common_top" .}} <input type="hidden" name="revert" value="{{if eq .CherryPickType "revert"}}true{{else}}false{{end}}"> <div class="repo-editor-header"> - <div class="ui breadcrumb field {{if .Err_TreePath}}error{{end}}"> - {{$shaurl := printf "%s/commit/%s" $.RepoLink (PathEscape .SHA)}} - {{$shalink := HTMLFormat `<a class="ui primary sha label" href="%s">%s</a>` $shaurl (ShortSha .SHA)}} + <div class="breadcrumb"> + {{$shaurl := printf "%s/commit/%s" $.RepoLink (PathEscape .FromCommitID)}} + {{$shalink := HTMLFormat `<a class="ui primary sha label" href="%s">%s</a>` $shaurl (ShortSha .FromCommitID)}} {{if eq .CherryPickType "revert"}} {{ctx.Locale.Tr "repo.editor.revert" $shalink}} {{else}} diff --git a/templates/repo/editor/commit_form.tmpl b/templates/repo/editor/commit_form.tmpl index 7efed77349..7067614444 100644 --- a/templates/repo/editor/commit_form.tmpl +++ b/templates/repo/editor/commit_form.tmpl @@ -1,11 +1,11 @@ <div class="commit-form-wrapper"> {{ctx.AvatarUtils.Avatar .SignedUser 40 "commit-avatar"}} <div class="commit-form"> - <h3>{{- if .CanCommitToBranch.WillSign}} - <span title="{{ctx.Locale.Tr "repo.signing.will_sign" .CanCommitToBranch.SigningKey}}">{{svg "octicon-lock" 24}}</span> + <h3>{{- if .CommitFormOptions.WillSign}} + <span title="{{ctx.Locale.Tr "repo.signing.will_sign" .CommitFormOptions.SigningKey}}">{{svg "octicon-lock" 24}}</span> {{ctx.Locale.Tr "repo.editor.commit_signed_changes"}} {{- else}} - <span title="{{ctx.Locale.Tr (printf "repo.signing.wont_sign.%s" .CanCommitToBranch.WontSignReason)}}">{{svg "octicon-unlock" 24}}</span> + <span title="{{ctx.Locale.Tr (printf "repo.signing.wont_sign.%s" .CommitFormOptions.WontSignReason)}}">{{svg "octicon-unlock" 24}}</span> {{ctx.Locale.Tr "repo.editor.commit_changes"}} {{- end}}</h3> <div class="field"> @@ -22,17 +22,17 @@ </div> <div class="quick-pull-choice js-quick-pull-choice"> <div class="field"> - <div class="ui radio checkbox {{if not .CanCommitToBranch.CanCommitToBranch}}disabled{{end}}"> + <div class="ui radio checkbox {{if not .CommitFormOptions.CanCommitToBranch}}disabled{{end}}"> <input type="radio" class="js-quick-pull-choice-option" name="commit_choice" value="direct" data-button-text="{{ctx.Locale.Tr "repo.editor.commit_changes"}}" {{if eq .commit_choice "direct"}}checked{{end}}> <label> {{svg "octicon-git-commit"}} {{ctx.Locale.Tr "repo.editor.commit_directly_to_this_branch" .BranchName}} - {{if not .CanCommitToBranch.CanCommitToBranch}} + {{if not .CommitFormOptions.CanCommitToBranch}} <div class="ui visible small warning message"> {{ctx.Locale.Tr "repo.editor.no_commit_to_branch"}} <ul> - {{if not .CanCommitToBranch.UserCanPush}}<li>{{ctx.Locale.Tr "repo.editor.user_no_push_to_branch"}}</li>{{end}} - {{if and .CanCommitToBranch.RequireSigned (not .CanCommitToBranch.WillSign)}}<li>{{ctx.Locale.Tr "repo.editor.require_signed_commit"}}</li>{{end}} + {{if not .CommitFormOptions.UserCanPush}}<li>{{ctx.Locale.Tr "repo.editor.user_no_push_to_branch"}}</li>{{end}} + {{if and .CommitFormOptions.RequireSigned (not .CommitFormOptions.WillSign)}}<li>{{ctx.Locale.Tr "repo.editor.require_signed_commit"}}</li>{{end}} </ul> </div> {{end}} @@ -42,14 +42,14 @@ {{if and (not .Repository.IsEmpty) (not .IsEditingFileOnly)}} <div class="field"> <div class="ui radio checkbox"> - {{if .CanCreatePullRequest}} + {{if .CommitFormOptions.CanCreatePullRequest}} <input type="radio" class="js-quick-pull-choice-option" name="commit_choice" value="commit-to-new-branch" data-button-text="{{ctx.Locale.Tr "repo.editor.propose_file_change"}}" {{if eq .commit_choice "commit-to-new-branch"}}checked{{end}}> {{else}} <input type="radio" class="js-quick-pull-choice-option" name="commit_choice" value="commit-to-new-branch" data-button-text="{{ctx.Locale.Tr "repo.editor.commit_changes"}}" {{if eq .commit_choice "commit-to-new-branch"}}checked{{end}}> {{end}} <label> {{svg "octicon-git-pull-request"}} - {{if .CanCreatePullRequest}} + {{if .CommitFormOptions.CanCreatePullRequest}} {{ctx.Locale.Tr "repo.editor.create_new_branch"}} {{else}} {{ctx.Locale.Tr "repo.editor.create_new_branch_np"}} @@ -58,7 +58,7 @@ </div> </div> <div class="quick-pull-branch-name {{if not (eq .commit_choice "commit-to-new-branch")}}tw-hidden{{end}}"> - <div class="new-branch-name-input field {{if .Err_NewBranchName}}error{{end}}"> + <div class="new-branch-name-input field"> {{svg "octicon-git-branch"}} <input type="text" name="new_branch_name" maxlength="100" value="{{.new_branch_name}}" class="input-contrast tw-mr-1 js-quick-pull-new-branch-name" placeholder="{{ctx.Locale.Tr "repo.editor.new_branch_name_desc"}}" {{if eq .commit_choice "commit-to-new-branch"}}required{{end}} title="{{ctx.Locale.Tr "repo.editor.new_branch_name"}}"> <span class="text-muted js-quick-pull-normalization-info"></span> @@ -67,7 +67,7 @@ {{end}} </div> {{if and .CommitCandidateEmails (gt (len .CommitCandidateEmails) 1)}} - <div class="field {{if .Err_CommitEmail}}error{{end}}"> + <div class="field"> <label>{{ctx.Locale.Tr "repo.editor.commit_email"}}</label> <select class="ui selection dropdown" name="commit_email"> {{- range $email := .CommitCandidateEmails -}} @@ -77,7 +77,8 @@ </div> {{end}} </div> - <button id="commit-button" type="submit" class="ui primary button" {{if .PageIsEdit}}disabled{{end}}> + <input type="hidden" name="last_commit" value="{{.last_commit}}"> + <button id="commit-button" type="submit" class="ui primary button"> {{if eq .commit_choice "commit-to-new-branch"}}{{ctx.Locale.Tr "repo.editor.propose_file_change"}}{{else}}{{ctx.Locale.Tr "repo.editor.commit_changes"}}{{end}} </button> <a class="ui button red" href="{{if .ReturnURI}}{{.ReturnURI}}{{else}}{{$.BranchLink}}/{{PathEscapeSegments .TreePath}}{{end}}">{{ctx.Locale.Tr "repo.editor.cancel"}}</a> diff --git a/templates/repo/editor/common_breadcrumb.tmpl b/templates/repo/editor/common_breadcrumb.tmpl new file mode 100644 index 0000000000..df36f00504 --- /dev/null +++ b/templates/repo/editor/common_breadcrumb.tmpl @@ -0,0 +1,16 @@ +<div class="breadcrumb"> + <a class="section" href="{{$.BranchLink}}">{{.Repository.Name}}</a> + {{$n := len .TreeNames}} + {{$l := Eval $n "-" 1}} + {{range $i, $v := .TreeNames}} + <div class="breadcrumb-divider">/</div> + {{if eq $i $l}} + <input id="file-name" maxlength="255" value="{{$v}}" placeholder="{{ctx.Locale.Tr (Iif $.PageIsUpload "repo.editor.add_subdir" "repo.editor.name_your_file")}}" data-editorconfig="{{$.EditorconfigJson}}" required autofocus> + <span data-tooltip-content="{{ctx.Locale.Tr "repo.editor.filename_help"}}">{{svg "octicon-info"}}</span> + {{else}} + <span class="section"><a href="{{$.BranchLink}}/{{index $.TreePaths $i | PathEscapeSegments}}">{{$v}}</a></span> + {{end}} + {{end}} + <span>{{ctx.Locale.Tr "repo.editor.or"}} <a href="{{or .ReturnURI (print $.BranchLink "/" (PathEscapeSegments .TreePath))}}">{{ctx.Locale.Tr "repo.editor.cancel_lower"}}</a></span> + <input type="hidden" id="tree_path" name="tree_path" value="{{.TreePath}}"> +</div> diff --git a/templates/repo/editor/common_top.tmpl b/templates/repo/editor/common_top.tmpl new file mode 100644 index 0000000000..23280ed565 --- /dev/null +++ b/templates/repo/editor/common_top.tmpl @@ -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}} diff --git a/templates/repo/editor/delete.tmpl b/templates/repo/editor/delete.tmpl index 2c0c2fc792..bf6143f1cb 100644 --- a/templates/repo/editor/delete.tmpl +++ b/templates/repo/editor/delete.tmpl @@ -3,9 +3,9 @@ {{template "repo/header" .}} <div class="ui container"> {{template "base/alert" .}} - <form class="ui form" method="post"> + <form class="ui form form-fetch-action" method="post" action="{{.CommitFormOptions.TargetFormAction}}"> {{.CsrfTokenHtml}} - <input type="hidden" name="last_commit" value="{{.last_commit}}"> + {{template "repo/editor/common_top" .}} {{template "repo/editor/commit_form" .}} </form> </div> diff --git a/templates/repo/editor/edit.tmpl b/templates/repo/editor/edit.tmpl index e1bf46d53d..0911d02e1f 100644 --- a/templates/repo/editor/edit.tmpl +++ b/templates/repo/editor/edit.tmpl @@ -3,30 +3,14 @@ {{template "repo/header" .}} <div class="ui container"> {{template "base/alert" .}} - <form class="ui edit form" method="post" + <form class="ui edit form form-fetch-action" method="post" action="{{.CommitFormOptions.TargetFormAction}}" data-text-empty-confirm-header="{{ctx.Locale.Tr "repo.editor.commit_empty_file_header"}}" data-text-empty-confirm-content="{{ctx.Locale.Tr "repo.editor.commit_empty_file_text"}}" > {{.CsrfTokenHtml}} - <input type="hidden" name="last_commit" value="{{.last_commit}}"> - <input type="hidden" name="page_has_posted" value="{{.PageHasPosted}}"> + {{template "repo/editor/common_top" .}} <div class="repo-editor-header"> - <div class="ui breadcrumb field{{if .Err_TreePath}} error{{end}}"> - <a class="section" href="{{$.BranchLink}}">{{.Repository.Name}}</a> - {{$n := len .TreeNames}} - {{$l := Eval $n "-" 1}} - {{range $i, $v := .TreeNames}} - <div class="breadcrumb-divider">/</div> - {{if eq $i $l}} - <input id="file-name" maxlength="255" value="{{$v}}" placeholder="{{ctx.Locale.Tr "repo.editor.name_your_file"}}" data-editorconfig="{{$.EditorconfigJson}}" required autofocus> - <span data-tooltip-content="{{ctx.Locale.Tr "repo.editor.filename_help"}}">{{svg "octicon-info"}}</span> - {{else}} - <span class="section"><a href="{{$.BranchLink}}/{{index $.TreePaths $i | PathEscapeSegments}}">{{$v}}</a></span> - {{end}} - {{end}} - <span>{{ctx.Locale.Tr "repo.editor.or"}} <a href="{{if .ReturnURI}}{{.ReturnURI}}{{else}}{{$.BranchLink}}{{if not .IsNewFile}}/{{PathEscapeSegments .TreePath}}{{end}}{{end}}">{{ctx.Locale.Tr "repo.editor.cancel_lower"}}</a></span> - <input type="hidden" id="tree_path" name="tree_path" value="{{.TreePath}}" required> - </div> + {{template "repo/editor/common_breadcrumb" .}} </div> {{if not .NotEditableReason}} <div class="field"> diff --git a/templates/repo/editor/fork.tmpl b/templates/repo/editor/fork.tmpl new file mode 100644 index 0000000000..e28b2ba7a2 --- /dev/null +++ b/templates/repo/editor/fork.tmpl @@ -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" .}} diff --git a/templates/repo/editor/patch.tmpl b/templates/repo/editor/patch.tmpl index 33a7c2a89d..fa00edd92e 100644 --- a/templates/repo/editor/patch.tmpl +++ b/templates/repo/editor/patch.tmpl @@ -3,15 +3,14 @@ {{template "repo/header" .}} <div class="ui container"> {{template "base/alert" .}} - <form class="ui edit form" method="post" action="{{.RepoLink}}/_diffpatch/{{.BranchName | PathEscapeSegments}}" + <form class="ui edit form form-fetch-action" method="post" action="{{.CommitFormOptions.TargetFormAction}}" data-text-empty-confirm-header="{{ctx.Locale.Tr "repo.editor.commit_empty_file_header"}}" data-text-empty-confirm-content="{{ctx.Locale.Tr "repo.editor.commit_empty_file_text"}}" > {{.CsrfTokenHtml}} - <input type="hidden" name="last_commit" value="{{.last_commit}}"> - <input type="hidden" name="page_has_posted" value="{{.PageHasPosted}}"> + {{template "repo/editor/common_top" .}} <div class="repo-editor-header"> - <div class="ui breadcrumb field {{if .Err_TreePath}}error{{end}}"> + <div class="breadcrumb"> {{ctx.Locale.Tr "repo.editor.patching"}} <a class="section" href="{{$.RepoLink}}">{{.Repository.FullName}}</a> <div class="breadcrumb-divider">:</div> diff --git a/templates/repo/editor/upload.tmpl b/templates/repo/editor/upload.tmpl index 5725020406..3e36c77b3b 100644 --- a/templates/repo/editor/upload.tmpl +++ b/templates/repo/editor/upload.tmpl @@ -3,25 +3,11 @@ {{template "repo/header" .}} <div class="ui container"> {{template "base/alert" .}} - <form class="ui comment form" method="post"> + <form class="ui comment form form-fetch-action" method="post" action="{{.CommitFormOptions.TargetFormAction}}"> {{.CsrfTokenHtml}} + {{template "repo/editor/common_top" .}} <div class="repo-editor-header"> - <div class="ui breadcrumb field {{if .Err_TreePath}}error{{end}}"> - <a class="section" href="{{$.BranchLink}}">{{.Repository.Name}}</a> - {{$n := len .TreeNames}} - {{$l := Eval $n "-" 1}} - {{range $i, $v := .TreeNames}} - <div class="breadcrumb-divider">/</div> - {{if eq $i $l}} - <input type="text" id="file-name" maxlength="255" value="{{$v}}" placeholder="{{ctx.Locale.Tr "repo.editor.add_subdir"}}" autofocus> - <span data-tooltip-content="{{ctx.Locale.Tr "repo.editor.filename_help"}}">{{svg "octicon-info"}}</span> - {{else}} - <span class="section"><a href="{{$.BranchLink}}/{{index $.TreePaths $i | PathEscapeSegments}}">{{$v}}</a></span> - {{end}} - {{end}} - <span>{{ctx.Locale.Tr "repo.editor.or"}} <a href="{{$.BranchLink}}{{if not .IsNewFile}}/{{.TreePath | PathEscapeSegments}}{{end}}">{{ctx.Locale.Tr "repo.editor.cancel_lower"}}</a></span> - <input type="hidden" id="tree_path" name="tree_path" value="{{.TreePath}}" required> - </div> + {{template "repo/editor/common_breadcrumb" .}} </div> <div class="field"> {{template "repo/upload" .}} diff --git a/templates/repo/forks.tmpl b/templates/repo/forks.tmpl index 725b67c651..6e46457893 100644 --- a/templates/repo/forks.tmpl +++ b/templates/repo/forks.tmpl @@ -1,20 +1,12 @@ {{template "base/head" .}} <div role="main" aria-label="{{.Title}}" class="page-content repository forks"> {{template "repo/header" .}} - <div class="ui container"> + <div class="ui container fork-list"> <h2 class="ui dividing header"> {{ctx.Locale.Tr "repo.forks"}} </h2> - <div class="flex-list"> - {{range .Forks}} - <div class="flex-item tw-border-0 repo-fork-item"> - <span>{{ctx.AvatarUtils.Avatar .Owner}}</span> - <span><a href="{{.Owner.HomeLink}}">{{.Owner.Name}}</a> / <a href="{{.Link}}">{{.Name}}</a></span> - </div> - {{end}} - </div> + {{template "shared/repo/list" .}} + {{template "base/paginate" .}} </div> - - {{template "base/paginate" .}} </div> {{template "base/footer" .}} diff --git a/templates/repo/header.tmpl b/templates/repo/header.tmpl index 2fbb6ba428..b61076ff46 100644 --- a/templates/repo/header.tmpl +++ b/templates/repo/header.tmpl @@ -230,7 +230,7 @@ <div class="overflow-menu-items"> {{if(and .Repository.IsBeingCreated (.Permission.CanRead ctx.Consts.RepoUnitTypeCode))}} <a class="{{if not .PageIsRepoSettings}}active {{end}}item" href="{{.RepoLink}}"> - {{svg "octicon-clock"}} {{ctx.Locale.Tr "repo.migrating_status"}} + {{svg "octicon-clock"}} {{ctx.Locale.Tr "repo.migration_status"}} </a> {{end}} diff --git a/templates/repo/issue/card.tmpl b/templates/repo/issue/card.tmpl index 41fe6cea8f..9a8341dcca 100644 --- a/templates/repo/issue/card.tmpl +++ b/templates/repo/issue/card.tmpl @@ -4,7 +4,7 @@ {{if $attachments}} <div class="card-attachment-images"> {{range $attachments}} - <img src="{{.DownloadURL}}" alt="{{.Name}}" /> + <img loading="lazy" src="{{.DownloadURL}}" alt="{{.Name}}" /> {{end}} </div> {{end}} diff --git a/templates/repo/issue/view_content/attachments.tmpl b/templates/repo/issue/view_content/attachments.tmpl index 2155f78656..e865050559 100644 --- a/templates/repo/issue/view_content/attachments.tmpl +++ b/templates/repo/issue/view_content/attachments.tmpl @@ -31,7 +31,7 @@ {{if FilenameIsImage .Name}} {{if not (StringUtils.Contains (StringUtils.ToString $.RenderedContent) .UUID)}} <a target="_blank" rel="noopener noreferrer" href="{{.DownloadURL}}"> - <img alt="{{.Name}}" src="{{.DownloadURL}}" title="{{ctx.Locale.Tr "repo.issues.attachment.open_tab" .Name}}"> + <img loading="lazy" alt="{{.Name}}" src="{{.DownloadURL}}" title="{{ctx.Locale.Tr "repo.issues.attachment.open_tab" .Name}}"> </a> {{end}} {{end}} diff --git a/templates/repo/issue/view_content/comments.tmpl b/templates/repo/issue/view_content/comments.tmpl index 8f49bcf07e..6f61d88d3b 100644 --- a/templates/repo/issue/view_content/comments.tmpl +++ b/templates/repo/issue/view_content/comments.tmpl @@ -163,6 +163,7 @@ </span> <div class="detail flex-text-block"> {{svg "octicon-git-commit"}} + {{/* the content is a link like <a href="{RepoLink}/commit/{CommitID}">message title</a> (from CreateRefComment) */}} <span class="text grey muted-links">{{.Content | SanitizeHTML}}</span> </div> </div> @@ -615,7 +616,7 @@ <div class="timeline-item-group"> <div class="timeline-item event" id="{{.HashTag}}"> <a class="timeline-avatar"{{if gt .Poster.ID 0}} href="{{.Poster.HomeLink}}"{{end}}> - <img alt src="{{.Poster.AvatarLink ctx}}" width="40" height="40"> + <img loading="lazy" alt src="{{.Poster.AvatarLink ctx}}" width="40" height="40"> </a> <span class="badge grey">{{svg "octicon-x" 16}}</span> <span class="text grey muted-links"> diff --git a/templates/repo/settings/lfs_file.tmpl b/templates/repo/settings/lfs_file.tmpl index 7698f77b2a..1a8014e218 100644 --- a/templates/repo/settings/lfs_file.tmpl +++ b/templates/repo/settings/lfs_file.tmpl @@ -21,7 +21,7 @@ {{else if not .IsTextFile}} <div class="view-raw"> {{if .IsImageFile}} - <img alt="{{$.RawFileLink}}" src="{{$.RawFileLink}}"> + <img loading="lazy" alt="{{$.RawFileLink}}" src="{{$.RawFileLink}}"> {{else if .IsVideoFile}} <video controls src="{{$.RawFileLink}}"> <strong>{{ctx.Locale.Tr "repo.video_not_supported_in_browser"}}</strong> diff --git a/templates/repo/settings/webhook/settings.tmpl b/templates/repo/settings/webhook/settings.tmpl index b8d9609391..a330448c9e 100644 --- a/templates/repo/settings/webhook/settings.tmpl +++ b/templates/repo/settings/webhook/settings.tmpl @@ -298,7 +298,7 @@ <label for="authorization_header">{{ctx.Locale.Tr "repo.settings.authorization_header"}}</label> <input id="authorization_header" name="authorization_header" type="text" value="{{.Webhook.HeaderAuthorization}}"{{if eq .HookType "matrix"}} placeholder="Bearer $access_token" required{{end}}> {{if ne .HookType "matrix"}}{{/* Matrix doesn't make the authorization optional but it is implied by the help string, should be changed.*/}} - <span class="help">{{ctx.Locale.Tr "repo.settings.authorization_header_desc" ("<code>Bearer token123456</code>, <code>Basic YWxhZGRpbjpvcGVuc2VzYW1l</code>" | SafeHTML)}}</span> + <span class="help">{{ctx.Locale.Tr "repo.settings.authorization_header_desc" (HTMLFormat "<code>%s</code>, <code>%s</code>" "Bearer token123456" "Basic YWxhZGRpbjpvcGVuc2VzYW1l")}}</span> {{end}} </div> diff --git a/templates/repo/unicode_escape_prompt.tmpl b/templates/repo/unicode_escape_prompt.tmpl index 8bceafa8bb..f8226ec728 100644 --- a/templates/repo/unicode_escape_prompt.tmpl +++ b/templates/repo/unicode_escape_prompt.tmpl @@ -1,22 +1,22 @@ {{if .EscapeStatus}} {{if .EscapeStatus.HasInvisible}} - <div class="ui warning message unicode-escape-prompt tw-text-left"> + <div class="ui warning message unicode-escape-prompt"> <button class="btn close icon hide-panel" data-panel-closest=".message">{{svg "octicon-x" 16 "close inside"}}</button> <div class="header"> {{ctx.Locale.Tr "repo.invisible_runes_header"}} </div> - <p>{{ctx.Locale.Tr "repo.invisible_runes_description"}}</p> + <div>{{ctx.Locale.Tr "repo.invisible_runes_description"}}</div> {{if .EscapeStatus.HasAmbiguous}} - <p>{{ctx.Locale.Tr "repo.ambiguous_runes_description"}}</p> + <div>{{ctx.Locale.Tr "repo.ambiguous_runes_description"}}</div> {{end}} </div> {{else if .EscapeStatus.HasAmbiguous}} - <div class="ui warning message unicode-escape-prompt tw-text-left"> + <div class="ui warning message unicode-escape-prompt"> <button class="btn close icon hide-panel" data-panel-closest=".message">{{svg "octicon-x" 16 "close inside"}}</button> <div class="header"> {{ctx.Locale.Tr "repo.ambiguous_runes_header"}} </div> - <p>{{ctx.Locale.Tr "repo.ambiguous_runes_description"}}</p> + <div>{{ctx.Locale.Tr "repo.ambiguous_runes_description"}}</div> </div> {{end}} {{end}} diff --git a/templates/repo/view_content.tmpl b/templates/repo/view_content.tmpl index 292a2f878c..3ba04a9974 100644 --- a/templates/repo/view_content.tmpl +++ b/templates/repo/view_content.tmpl @@ -41,8 +41,8 @@ <a href="{{.Repository.Link}}/find/{{.RefTypeNameSubURL}}" class="ui compact basic button">{{ctx.Locale.Tr "repo.find_file.go_to_file"}}</a> {{end}} - {{if and .CanWriteCode .RefFullName.IsBranch (not .Repository.IsMirror) (not .Repository.IsArchived) (not .IsViewFile)}} - <button class="ui dropdown basic compact jump button"{{if not .Repository.CanEnableEditor}} disabled{{end}}> + {{if and .RefFullName.IsBranch (not .IsViewFile)}} + <button class="ui dropdown basic compact jump button repo-add-file" {{if not .Repository.CanEnableEditor}}disabled{{end}}> {{ctx.Locale.Tr "repo.editor.add_file"}} {{svg "octicon-triangle-down" 14 "dropdown icon"}} <div class="menu"> @@ -69,7 +69,7 @@ {{if not $isTreePathRoot}} {{$treeNameIdxLast := Eval (len .TreeNames) "-" 1}} - <span class="breadcrumb repo-path tw-ml-1"> + <span class="breadcrumb"> <a class="section" href="{{.RepoLink}}/src/{{.RefTypeNameSubURL}}" title="{{.Repository.Name}}">{{StringUtils.EllipsisString .Repository.Name 30}}</a> {{- range $i, $v := .TreeNames -}} <span class="breadcrumb-divider">/</span> diff --git a/templates/repo/wiki/new.tmpl b/templates/repo/wiki/new.tmpl index 5ebccc69e9..12f0983904 100644 --- a/templates/repo/wiki/new.tmpl +++ b/templates/repo/wiki/new.tmpl @@ -18,7 +18,7 @@ {{ctx.Locale.Tr "repo.wiki.page_name_desc"}} </div> - {{$content := .content}} + {{$content := .WikiEditContent}} {{if not .PageIsWikiEdit}} {{$content = ctx.Locale.Tr "repo.wiki.welcome"}} {{end}} diff --git a/templates/repo/wiki/view.tmpl b/templates/repo/wiki/view.tmpl index 89befcd7c5..f6f82fb52d 100644 --- a/templates/repo/wiki/view.tmpl +++ b/templates/repo/wiki/view.tmpl @@ -62,36 +62,34 @@ {{end}} <div class="wiki-content-parts"> - {{if .sidebarTocContent}} + {{if .WikiSidebarTocHTML}} <div class="render-content markup wiki-content-sidebar wiki-content-toc"> - {{.sidebarTocContent | SafeHTML}} + {{.WikiSidebarTocHTML}} </div> {{end}} - <div class="render-content markup wiki-content-main {{if or .sidebarTocContent .sidebarPresent}}with-sidebar{{end}}"> - {{template "repo/unicode_escape_prompt" dict "EscapeStatus" .EscapeStatus "root" $}} - {{.content | SafeHTML}} + <div class="render-content markup wiki-content-main {{if or .WikiSidebarTocHTML .WikiSidebarHTML}}with-sidebar{{end}}"> + {{template "repo/unicode_escape_prompt" dict "EscapeStatus" .EscapeStatus}} + {{.WikiContentHTML}} </div> - {{if .sidebarPresent}} + {{if .WikiSidebarHTML}} <div class="render-content markup wiki-content-sidebar"> {{if and .CanWriteWiki (not .Repository.IsMirror)}} <a class="tw-float-right muted" href="{{.RepoLink}}/wiki/_Sidebar?action=_edit" aria-label="{{ctx.Locale.Tr "repo.wiki.edit_page_button"}}">{{svg "octicon-pencil"}}</a> {{end}} - {{template "repo/unicode_escape_prompt" dict "EscapeStatus" .sidebarEscapeStatus "root" $}} - {{.sidebarContent | SafeHTML}} + {{.WikiSidebarHTML}} </div> {{end}} <div class="tw-clear-both"></div> - {{if .footerPresent}} + {{if .WikiFooterHTML}} <div class="render-content markup wiki-content-footer"> {{if and .CanWriteWiki (not .Repository.IsMirror)}} <a class="tw-float-right muted" href="{{.RepoLink}}/wiki/_Footer?action=_edit" aria-label="{{ctx.Locale.Tr "repo.wiki.edit_page_button"}}">{{svg "octicon-pencil"}}</a> {{end}} - {{template "repo/unicode_escape_prompt" dict "footerEscapeStatus" .sidebarEscapeStatus "root" $}} - {{.footerContent | SafeHTML}} + {{.WikiFooterHTML}} </div> {{end}} </div> diff --git a/templates/explore/repo_list.tmpl b/templates/shared/repo/list.tmpl index ad190b89b2..2c8af14f9c 100644 --- a/templates/explore/repo_list.tmpl +++ b/templates/shared/repo/list.tmpl @@ -2,12 +2,16 @@ {{range .Repos}} <div class="flex-item"> <div class="flex-item-leading"> - {{template "repo/icon" .}} + {{if $.ShowRepoOwnerAvatar}} + {{ctx.AvatarUtils.Avatar .Owner 24}} + {{else}} + {{template "repo/icon" .}} + {{end}} </div> <div class="flex-item-main"> <div class="flex-item-header"> <div class="flex-item-title"> - {{if and (or $.PageIsExplore $.PageIsProfileStarList) .Owner}} + {{if and $.ShowRepoOwnerOnList .Owner}} <a class="text primary name" href="{{.Owner.HomeLink}}">{{.Owner.Name}}</a>/ {{end}} <a class="text primary name" href="{{.Link}}">{{.Name}}</a> diff --git a/templates/shared/repo_search.tmpl b/templates/shared/repo/search.tmpl index a909061184..a909061184 100644 --- a/templates/shared/repo_search.tmpl +++ b/templates/shared/repo/search.tmpl diff --git a/templates/shared/user/profile_big_avatar.tmpl b/templates/shared/user/profile_big_avatar.tmpl index 91f04e0b53..1190dc54ec 100644 --- a/templates/shared/user/profile_big_avatar.tmpl +++ b/templates/shared/user/profile_big_avatar.tmpl @@ -103,7 +103,7 @@ <ul class="user-badges"> {{range .Badges}} <li> - <img width="64" height="64" src="{{.ImageURL}}" alt="{{.Description}}" data-tooltip-content="{{.Description}}"> + <img loading="lazy" width="64" height="64" src="{{.ImageURL}}" alt="{{.Description}}" data-tooltip-content="{{.Description}}"> </li> {{end}} </ul> diff --git a/templates/user/auth/signup_inner.tmpl b/templates/user/auth/signup_inner.tmpl index d66568199d..a3f6e1471f 100644 --- a/templates/user/auth/signup_inner.tmpl +++ b/templates/user/auth/signup_inner.tmpl @@ -7,6 +7,9 @@ {{end}} </h4> <div class="ui attached segment"> + {{if .IsFirstTimeRegistration}} + <p>{{ctx.Locale.Tr "auth.sign_up_tip"}}</p> + {{end}} <form class="ui form" action="{{.SignUpLink}}" method="post"> {{.CsrfTokenHtml}} {{if or (not .LinkAccountMode) (and .LinkAccountMode .LinkAccountModeRegister)}} diff --git a/templates/user/dashboard/feeds.tmpl b/templates/user/dashboard/feeds.tmpl index 4ee12fa783..37dede7af6 100644 --- a/templates/user/dashboard/feeds.tmpl +++ b/templates/user/dashboard/feeds.tmpl @@ -91,7 +91,7 @@ {{range $push.Commits}} {{$commitLink := printf "%s/commit/%s" $repoLink .Sha1}} <div class="flex-text-block"> - <img alt class="ui avatar" src="{{$push.AvatarLink ctx .AuthorEmail}}" title="{{.AuthorName}}" width="16" height="16"> + <img loading="lazy" alt class="ui avatar" src="{{$push.AvatarLink ctx .AuthorEmail}}" title="{{.AuthorName}}" width="16" height="16"> <a class="ui sha label" href="{{$commitLink}}">{{ShortSha .Sha1}}</a> <span class="text truncate"> {{ctx.RenderUtils.RenderCommitMessage .Message $repo}} diff --git a/templates/user/notification/notification_subscriptions.tmpl b/templates/user/notification/notification_subscriptions.tmpl index 6ef53ab654..e4dd27e63b 100644 --- a/templates/user/notification/notification_subscriptions.tmpl +++ b/templates/user/notification/notification_subscriptions.tmpl @@ -69,8 +69,8 @@ {{template "shared/issuelist" dict "." . "listType" "dashboard"}} {{end}} {{else}} - {{template "shared/repo_search" .}} - {{template "explore/repo_list" .}} + {{template "shared/repo/search" .}} + {{template "shared/repo/list" .}} {{template "base/paginate" .}} {{end}} </div> diff --git a/templates/user/profile.tmpl b/templates/user/profile.tmpl index e5c3412ddd..74a53b937d 100644 --- a/templates/user/profile.tmpl +++ b/templates/user/profile.tmpl @@ -17,8 +17,8 @@ {{template "user/dashboard/feeds" .}} {{else if eq .TabName "stars"}} <div class="stars"> - {{template "shared/repo_search" .}} - {{template "explore/repo_list" .}} + {{template "shared/repo/search" .}} + {{template "shared/repo/list" .}} {{template "base/paginate" .}} </div> {{else if eq .TabName "following"}} @@ -30,8 +30,8 @@ {{else if eq .TabName "organizations"}} {{template "repo/user_cards" .}} {{else}} - {{template "shared/repo_search" .}} - {{template "explore/repo_list" .}} + {{template "shared/repo/search" .}} + {{template "shared/repo/list" .}} {{template "base/paginate" .}} {{end}} </div> diff --git a/templates/user/settings/applications.tmpl b/templates/user/settings/applications.tmpl index 501f238c7a..8c24da7fc9 100644 --- a/templates/user/settings/applications.tmpl +++ b/templates/user/settings/applications.tmpl @@ -68,7 +68,7 @@ </label> </div> <div> - <div class="tw-my-2">{{ctx.Locale.Tr "settings.access_token_desc" (HTMLFormat `href="%s/api/swagger" target="_blank"` AppSubUrl) (`href="https://docs.gitea.com/development/oauth2-provider#scopes" target="_blank"`|SafeHTML)}}</div> + <div class="tw-my-2">{{ctx.Locale.Tr "settings.access_token_desc" (HTMLFormat `href="%s/api/swagger" target="_blank"` AppSubUrl) (HTMLFormat `href="%s" target="_blank"` "https://docs.gitea.com/development/oauth2-provider#scopes")}}</div> <table class="ui table unstackable tw-my-2"> {{range $category := .TokenCategories}} <tr> diff --git a/templates/user/settings/organization.tmpl b/templates/user/settings/organization.tmpl index 16c27b52cd..a48ca9ec9b 100644 --- a/templates/user/settings/organization.tmpl +++ b/templates/user/settings/organization.tmpl @@ -47,7 +47,7 @@ {{ctx.Locale.Tr "org.members.leave"}} </div> <div class="content"> - <p>{{ctx.Locale.Tr "org.members.leave.detail" (`<span class="dataOrganizationName"></span>`|SafeHTML)}}</p> + <p>{{ctx.Locale.Tr "org.members.leave.detail" (HTMLFormat `<span class="%s"></span>` "dataOrganizationName")}}</p> </div> {{template "base/modal_actions_confirm" .}} </div> diff --git a/tests/integration/api_repo_languages_test.go b/tests/integration/api_repo_languages_test.go index 1045aef57d..6347a43b4e 100644 --- a/tests/integration/api_repo_languages_test.go +++ b/tests/integration/api_repo_languages_test.go @@ -9,6 +9,8 @@ import ( "testing" "time" + "code.gitea.io/gitea/modules/test" + "github.com/stretchr/testify/assert" ) @@ -32,7 +34,8 @@ func TestRepoLanguages(t *testing.T) { "content": "package main", "commit_choice": "direct", }) - session.MakeRequest(t, req, http.StatusSeeOther) + resp = session.MakeRequest(t, req, http.StatusOK) + assert.NotEmpty(t, test.RedirectURL(resp)) // let gitea calculate language stats time.Sleep(time.Second) diff --git a/tests/integration/api_repo_license_test.go b/tests/integration/api_repo_license_test.go index 52d3085694..fb4450a2bd 100644 --- a/tests/integration/api_repo_license_test.go +++ b/tests/integration/api_repo_license_test.go @@ -12,12 +12,13 @@ import ( auth_model "code.gitea.io/gitea/models/auth" api "code.gitea.io/gitea/modules/structs" + "code.gitea.io/gitea/modules/test" "github.com/stretchr/testify/assert" ) var testLicenseContent = ` -Copyright (c) 2024 Gitea +Copyright (c) 2024 Gitea Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: @@ -48,7 +49,8 @@ func TestAPIRepoLicense(t *testing.T) { "content": testLicenseContent, "commit_choice": "direct", }) - session.MakeRequest(t, req, http.StatusSeeOther) + resp = session.MakeRequest(t, req, http.StatusOK) + assert.NotEmpty(t, test.RedirectURL(resp)) // let gitea update repo license time.Sleep(time.Second) diff --git a/tests/integration/editor_test.go b/tests/integration/editor_test.go index a5936d86de..ac47ed0094 100644 --- a/tests/integration/editor_test.go +++ b/tests/integration/editor_test.go @@ -7,6 +7,7 @@ import ( "bytes" "fmt" "io" + "maps" "mime/multipart" "net/http" "net/http/httptest" @@ -19,289 +20,278 @@ import ( "code.gitea.io/gitea/models/unittest" user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/modules/git" - "code.gitea.io/gitea/modules/json" + "code.gitea.io/gitea/modules/test" "code.gitea.io/gitea/modules/translation" + "code.gitea.io/gitea/modules/util" "code.gitea.io/gitea/tests" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) -func TestCreateFile(t *testing.T) { +func TestEditor(t *testing.T) { onGiteaRun(t, func(t *testing.T, u *url.URL) { - session := loginUser(t, "user2") - testCreateFile(t, session, "user2", "repo1", "master", "test.txt", "Content") + sessionUser2 := loginUser(t, "user2") + t.Run("EditFileNotAllowed", testEditFileNotAllowed) + t.Run("DiffPreview", testEditorDiffPreview) + t.Run("CreateFile", testEditorCreateFile) + t.Run("EditFile", func(t *testing.T) { + testEditFile(t, sessionUser2, "user2", "repo1", "master", "README.md", "Hello, World (direct)\n") + testEditFileToNewBranch(t, sessionUser2, "user2", "repo1", "master", "feature/test", "README.md", "Hello, World (commit-to-new-branch)\n") + }) + t.Run("PatchFile", testEditorPatchFile) + t.Run("DeleteFile", func(t *testing.T) { + viewLink := "/user2/repo1/src/branch/branch2/README.md" + sessionUser2.MakeRequest(t, NewRequest(t, "GET", viewLink), http.StatusOK) + testEditorActionPostRequest(t, sessionUser2, "/user2/repo1/_delete/branch2/README.md", map[string]string{"commit_choice": "direct"}) + sessionUser2.MakeRequest(t, NewRequest(t, "GET", viewLink), http.StatusNotFound) + }) + t.Run("ForkToEditFile", func(t *testing.T) { + testForkToEditFile(t, loginUser(t, "user4"), "user4", "user2", "repo1", "master", "README.md") + }) + t.Run("WebGitCommitEmail", testEditorWebGitCommitEmail) + t.Run("ProtectedBranch", testEditorProtectedBranch) }) } -func testCreateFile(t *testing.T, session *TestSession, user, repo, branch, filePath, content string) *httptest.ResponseRecorder { - // Request editor page - newURL := fmt.Sprintf("/%s/%s/_new/%s/", user, repo, branch) - req := NewRequest(t, "GET", newURL) - resp := session.MakeRequest(t, req, http.StatusOK) - - doc := NewHTMLParser(t, resp.Body) - lastCommit := doc.GetInputValueByName("last_commit") - assert.NotEmpty(t, lastCommit) +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.`) +} - // Save new file to master branch - req = NewRequestWithValues(t, "POST", newURL, map[string]string{ - "_csrf": doc.GetCSRF(), - "last_commit": lastCommit, +func testCreateFile(t *testing.T, session *TestSession, user, repo, branch, filePath, content string) { + testEditorActionEdit(t, session, user, repo, "_new", branch, "", map[string]string{ "tree_path": filePath, "content": content, "commit_choice": "direct", }) - return session.MakeRequest(t, req, http.StatusSeeOther) } -func TestCreateFileOnProtectedBranch(t *testing.T) { - onGiteaRun(t, func(t *testing.T, u *url.URL) { - session := loginUser(t, "user2") - - csrf := GetUserCSRFToken(t, session) - // Change master branch to protected - req := NewRequestWithValues(t, "POST", "/user2/repo1/settings/branches/edit", map[string]string{ - "_csrf": csrf, - "rule_name": "master", - "enable_push": "true", - }) - session.MakeRequest(t, req, http.StatusSeeOther) - // Check if master branch has been locked successfully - flashMsg := session.GetCookieFlashMessage() - assert.Equal(t, `Branch protection for rule "master" has been updated.`, flashMsg.SuccessMsg) - - // Request editor page - req = NewRequest(t, "GET", "/user2/repo1/_new/master/") - 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", "/user2/repo1/_new/master/", map[string]string{ - "_csrf": doc.GetCSRF(), - "last_commit": lastCommit, - "tree_path": "test.txt", - "content": "Content", - "commit_choice": "direct", - }) - - resp = session.MakeRequest(t, req, http.StatusOK) - // Check body for error message - assert.Contains(t, resp.Body.String(), "Cannot commit to protected branch "master".") - - // 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 testEditorProtectedBranch(t *testing.T) { + session := loginUser(t, "user2") + // Change the "master" branch to "protected" + req := NewRequestWithValues(t, "POST", "/user2/repo1/settings/branches/edit", map[string]string{ + "_csrf": GetUserCSRFToken(t, session), + "rule_name": "master", + "enable_push": "true", }) + session.MakeRequest(t, req, http.StatusSeeOther) + flashMsg := session.GetCookieFlashMessage() + assert.Equal(t, `Branch protection for rule "master" has been updated.`, flashMsg.SuccessMsg) + + // Try to commit a file to the "master" branch and it should fail + resp := testEditorActionPostRequest(t, session, "/user2/repo1/_new/master/", map[string]string{"tree_path": "test-protected-branch.txt", "commit_choice": "direct"}) + assert.Equal(t, http.StatusBadRequest, resp.Code) + assert.Equal(t, `Cannot commit to protected branch "master".`, test.ParseJSONError(resp.Body.Bytes()).ErrorMessage) } -func testEditFile(t *testing.T, session *TestSession, user, repo, branch, filePath, newContent string) *httptest.ResponseRecorder { - // Get to the 'edit this file' page - req := NewRequest(t, "GET", path.Join(user, repo, "_edit", branch, filePath)) +func testEditorActionPostRequest(t *testing.T, session *TestSession, requestPath string, params map[string]string) *httptest.ResponseRecorder { + req := NewRequest(t, "GET", requestPath) resp := session.MakeRequest(t, req, http.StatusOK) - htmlDoc := NewHTMLParser(t, resp.Body) - lastCommit := htmlDoc.GetInputValueByName("last_commit") - assert.NotEmpty(t, lastCommit) - - // Submit the edits - req = NewRequestWithValues(t, "POST", path.Join(user, repo, "_edit", branch, filePath), - map[string]string{ - "_csrf": htmlDoc.GetCSRF(), - "last_commit": lastCommit, - "tree_path": filePath, - "content": newContent, - "commit_choice": "direct", - }, - ) - session.MakeRequest(t, req, http.StatusSeeOther) + 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) +} - // 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()) +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 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, - }, - ) - session.MakeRequest(t, req, http.StatusSeeOther) - - // 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()) +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", + }) +} - return resp +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 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 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 TestEditFileToNewBranch(t *testing.T) { - onGiteaRun(t, func(t *testing.T, u *url.URL) { - session := loginUser(t, "user2") - testEditFileToNewBranch(t, session, "user2", "repo1", "master", "feature/test", "README.md", "Hello, World (Edited)\n") +func 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 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 +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) - 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()) - htmlDoc := NewHTMLParser(t, resp.Body) - errMsg := htmlDoc.doc.Find(".ui.negative.message").Text() - assert.Contains(t, errMsg, translation.NewLocale("en-US").Tr("repo.editor.invalid_commit_email")) - } 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 - } + respMap := map[string]string{} + DecodeJSON(t, resp, &respMap) + return respMap["uuid"] + } + + t.Run("EmailInactive", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + email := unittest.AssertExistsAndLoadBean(t, &user_model.EmailAddress{ID: 35, UID: user.ID}) + require.False(t, email.IsActivated) + makeReq(t, "/user2/repo1/_edit/master/README.md", map[string]string{ + "tree_path": "README.md", + "content": "test content", + "commit_email": email.Email, + }, "", "") + }) - 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("EmailInvalid", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + email := unittest.AssertExistsAndLoadBean(t, &user_model.EmailAddress{ID: 1, IsActivated: true}) + require.NotEqual(t, email.UID, user.ID) + makeReq(t, "/user2/repo1/_edit/master/README.md", map[string]string{ + "tree_path": "README.md", + "content": "test content", + "commit_email": email.Email, + }, "", "") + }) - t.Run("EmailInactive", func(t *testing.T) { + testWebGit := func(t *testing.T, linkForKeepPrivate string, paramsForKeepPrivate map[string]string, linkForChosenEmail string, paramsForChosenEmail map[string]string) (resp1, resp2 *httptest.ResponseRecorder) { + t.Run("DefaultEmailKeepPrivate", func(t *testing.T) { defer tests.PrintCurrentTest(t)() - email := unittest.AssertExistsAndLoadBean(t, &user_model.EmailAddress{ID: 35, UID: user.ID}) - require.False(t, email.IsActivated) - makeReq(t, "/user2/repo1/_edit/master/README.md", map[string]string{ - "tree_path": "README.md", - "content": "test content", - "commit_email": email.Email, - }, "", "") + paramsForKeepPrivate["commit_email"] = "" + resp1 = makeReq(t, linkForKeepPrivate, paramsForKeepPrivate, "User Two", "user2@noreply.example.org") }) - - t.Run("EmailInvalid", func(t *testing.T) { + t.Run("ChooseEmail", func(t *testing.T) { defer tests.PrintCurrentTest(t)() - email := unittest.AssertExistsAndLoadBean(t, &user_model.EmailAddress{ID: 1, IsActivated: true}) - require.NotEqual(t, email.UID, user.ID) - makeReq(t, "/user2/repo1/_edit/master/README.md", map[string]string{ - "tree_path": "README.md", - "content": "test content", - "commit_email": email.Email, - }, "", "") - }) - - testWebGit := func(t *testing.T, linkForKeepPrivate string, paramsForKeepPrivate map[string]string, linkForChosenEmail string, paramsForChosenEmail map[string]string) (resp1, resp2 *httptest.ResponseRecorder) { - t.Run("DefaultEmailKeepPrivate", func(t *testing.T) { - defer tests.PrintCurrentTest(t)() - paramsForKeepPrivate["commit_email"] = "" - resp1 = makeReq(t, linkForKeepPrivate, paramsForKeepPrivate, "User Two", "user2@noreply.example.org") - }) - t.Run("ChooseEmail", func(t *testing.T) { - defer tests.PrintCurrentTest(t)() - paramsForChosenEmail["commit_email"] = "user2@example.com" - resp2 = makeReq(t, linkForChosenEmail, paramsForChosenEmail, "User Two", "user2@example.com") - }) - return resp1, resp2 - } - - t.Run("Edit", func(t *testing.T) { - testWebGit(t, - "/user2/repo1/_edit/master/README.md", map[string]string{"tree_path": "README.md", "content": "for keep private"}, - "/user2/repo1/_edit/master/README.md", map[string]string{"tree_path": "README.md", "content": "for chosen email"}, - ) + paramsForChosenEmail["commit_email"] = "user2@example.com" + resp2 = makeReq(t, linkForChosenEmail, paramsForChosenEmail, "User Two", "user2@example.com") }) + return resp1, resp2 + } + + t.Run("Edit", func(t *testing.T) { + testWebGit(t, + "/user2/repo1/_edit/master/README.md", map[string]string{"tree_path": "README.md", "content": "for keep private"}, + "/user2/repo1/_edit/master/README.md", map[string]string{"tree_path": "README.md", "content": "for chosen email"}, + ) + }) - t.Run("UploadDelete", func(t *testing.T) { - file1UUID := uploadFile(t, "file1", "File 1") - file2UUID := uploadFile(t, "file2", "File 2") - testWebGit(t, - "/user2/repo1/_upload/master", map[string]string{"files": file1UUID}, - "/user2/repo1/_upload/master", map[string]string{"files": file2UUID}, - ) - testWebGit(t, - "/user2/repo1/_delete/master/file1", map[string]string{}, - "/user2/repo1/_delete/master/file2", map[string]string{}, - ) - }) + t.Run("UploadDelete", func(t *testing.T) { + file1UUID := uploadFile(t, "file1", "File 1") + file2UUID := uploadFile(t, "file2", "File 2") + testWebGit(t, + "/user2/repo1/_upload/master", map[string]string{"files": file1UUID}, + "/user2/repo1/_upload/master", map[string]string{"files": file2UUID}, + ) + testWebGit(t, + "/user2/repo1/_delete/master/file1", map[string]string{}, + "/user2/repo1/_delete/master/file2", map[string]string{}, + ) + }) - t.Run("ApplyPatchCherryPick", func(t *testing.T) { - testWebGit(t, - "/user2/repo1/_diffpatch/master", map[string]string{ - "tree_path": "__dummy__", - "content": `diff --git a/patch-file-1.txt b/patch-file-1.txt + t.Run("ApplyPatchCherryPick", func(t *testing.T) { + testWebGit(t, + "/user2/repo1/_diffpatch/master", map[string]string{ + "tree_path": "__dummy__", + "content": `diff --git a/patch-file-1.txt b/patch-file-1.txt new file mode 100644 index 0000000000..aaaaaaaaaa --- /dev/null @@ -309,10 +299,10 @@ index 0000000000..aaaaaaaaaa @@ -0,0 +1 @@ +File 1 `, - }, - "/user2/repo1/_diffpatch/master", map[string]string{ - "tree_path": "__dummy__", - "content": `diff --git a/patch-file-2.txt b/patch-file-2.txt + }, + "/user2/repo1/_diffpatch/master", map[string]string{ + "tree_path": "__dummy__", + "content": `diff --git a/patch-file-2.txt b/patch-file-2.txt new file mode 100644 index 0000000000..bbbbbbbbbb --- /dev/null @@ -320,20 +310,146 @@ index 0000000000..bbbbbbbbbb @@ -0,0 +1 @@ +File 2 `, - }, - ) - - commit1, err := gitRepo.GetCommitByPath("patch-file-1.txt") - require.NoError(t, err) - commit2, err := gitRepo.GetCommitByPath("patch-file-2.txt") - require.NoError(t, err) - resp1, _ := testWebGit(t, - "/user2/repo1/_cherrypick/"+commit1.ID.String()+"/master", map[string]string{"revert": "true"}, - "/user2/repo1/_cherrypick/"+commit2.ID.String()+"/master", map[string]string{"revert": "true"}, - ) - - // By the way, test the "cherrypick" page: a successful revert redirects to the main branch - assert.Equal(t, "/user2/repo1/src/branch/master", resp1.Header().Get("Location")) - }) + }, + ) + + commit1, err := gitRepo.GetCommitByPath("patch-file-1.txt") + require.NoError(t, err) + commit2, err := gitRepo.GetCommitByPath("patch-file-2.txt") + require.NoError(t, err) + resp1, _ := testWebGit(t, + "/user2/repo1/_cherrypick/"+commit1.ID.String()+"/master", map[string]string{"revert": "true"}, + "/user2/repo1/_cherrypick/"+commit2.ID.String()+"/master", map[string]string{"revert": "true"}, + ) + + // By the way, test the "cherrypick" page: a successful revert redirects to the main branch + assert.Equal(t, "/user2/repo1/src/branch/master", test.RedirectURL(resp1)) }) } + +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) + }) + } +} diff --git a/tests/integration/empty_repo_test.go b/tests/integration/empty_repo_test.go index 8cebfaf32a..6a8c70f12f 100644 --- a/tests/integration/empty_repo_test.go +++ b/tests/integration/empty_repo_test.go @@ -10,7 +10,6 @@ import ( "io" "mime/multipart" "net/http" - "net/http/httptest" "strings" "testing" @@ -30,7 +29,7 @@ import ( "github.com/stretchr/testify/require" ) -func testAPINewFile(t *testing.T, session *TestSession, user, repo, branch, treePath, content string) *httptest.ResponseRecorder { +func testAPINewFile(t *testing.T, session *TestSession, user, repo, branch, treePath, content string) { url := fmt.Sprintf("/%s/%s/_new/%s", user, repo, branch) req := NewRequestWithValues(t, "POST", url, map[string]string{ "_csrf": GetUserCSRFToken(t, session), @@ -38,7 +37,8 @@ func testAPINewFile(t *testing.T, session *TestSession, user, repo, branch, tree "tree_path": treePath, "content": content, }) - return session.MakeRequest(t, req, http.StatusSeeOther) + resp := session.MakeRequest(t, req, http.StatusOK) + assert.NotEmpty(t, test.RedirectURL(resp)) } func TestEmptyRepo(t *testing.T) { @@ -87,7 +87,7 @@ func TestEmptyRepoAddFile(t *testing.T) { "content": "newly-added-test-file", }) - resp = session.MakeRequest(t, req, http.StatusSeeOther) + resp = session.MakeRequest(t, req, http.StatusOK) redirect := test.RedirectURL(resp) assert.Equal(t, "/user30/empty/src/branch/"+setting.Repository.DefaultBranch+"/test-file.md", redirect) @@ -154,9 +154,9 @@ func TestEmptyRepoUploadFile(t *testing.T) { "files": respMap["uuid"], "tree_path": "", }) - resp = session.MakeRequest(t, req, http.StatusSeeOther) + resp = session.MakeRequest(t, req, http.StatusOK) redirect := test.RedirectURL(resp) - assert.Equal(t, "/user30/empty/src/branch/"+setting.Repository.DefaultBranch+"/", redirect) + assert.Equal(t, "/user30/empty/src/branch/"+setting.Repository.DefaultBranch, redirect) req = NewRequest(t, "GET", redirect) resp = session.MakeRequest(t, req, http.StatusOK) diff --git a/tests/integration/html_helper.go b/tests/integration/html_helper.go index 874fc32228..4d589b32e7 100644 --- a/tests/integration/html_helper.go +++ b/tests/integration/html_helper.go @@ -42,7 +42,7 @@ func (doc *HTMLDoc) GetCSRF() string { return doc.GetInputValueByName("_csrf") } -// AssertHTMLElement check if element by selector exists or does not exist depending on checkExists +// AssertHTMLElement check if the element by selector exists or does not exist depending on checkExists func AssertHTMLElement[T int | bool](t testing.TB, doc *HTMLDoc, selector string, checkExists T) { sel := doc.doc.Find(selector) switch v := any(checkExists).(type) { diff --git a/tests/integration/pull_compare_test.go b/tests/integration/pull_compare_test.go index ba4cc82992..f95a2f1690 100644 --- a/tests/integration/pull_compare_test.go +++ b/tests/integration/pull_compare_test.go @@ -159,7 +159,8 @@ func TestPullCompare_EnableAllowEditsFromMaintainer(t *testing.T) { "commit_summary": "user2 updated the file", "commit_choice": "direct", }) - user2Session.MakeRequest(t, req, http.StatusSeeOther) + resp = user2Session.MakeRequest(t, req, http.StatusOK) + assert.NotEmpty(t, test.RedirectURL(resp)) } } }) diff --git a/tests/integration/pull_status_test.go b/tests/integration/pull_status_test.go index bfcb97b082..49326a594a 100644 --- a/tests/integration/pull_status_test.go +++ b/tests/integration/pull_status_test.go @@ -129,7 +129,7 @@ func TestPullCreate_EmptyChangesWithDifferentCommits(t *testing.T) { session := loginUser(t, "user1") testRepoFork(t, session, "user2", "repo1", "user1", "repo1", "") testEditFileToNewBranch(t, session, "user1", "repo1", "master", "status1", "README.md", "status1") - testEditFileToNewBranch(t, session, "user1", "repo1", "status1", "status1", "README.md", "# repo1\n\nDescription for repo1") + testEditFile(t, session, "user1", "repo1", "status1", "README.md", "# repo1\n\nDescription for repo1") url := path.Join("user1", "repo1", "compare", "master...status1") req := NewRequestWithValues(t, "POST", url, diff --git a/tests/integration/repo_fork_test.go b/tests/integration/repo_fork_test.go index db2caaf6ca..95325eefeb 100644 --- a/tests/integration/repo_fork_test.go +++ b/tests/integration/repo_fork_test.go @@ -84,7 +84,7 @@ func TestRepoForkToOrg(t *testing.T) { func TestForkListLimitedAndPrivateRepos(t *testing.T) { defer tests.PrepareTestEnv(t)() - forkItemSelector := ".repo-fork-item" + forkItemSelector := ".fork-list .flex-item" user1Sess := loginUser(t, "user1") user1 := unittest.AssertExistsAndLoadBean(t, &user_model.User{Name: "user1"}) diff --git a/web_src/css/base.css b/web_src/css/base.css index b50abf79f1..dc58fb850a 100644 --- a/web_src/css/base.css +++ b/web_src/css/base.css @@ -30,6 +30,10 @@ --page-spacing: 16px; /* space between page elements */ --page-margin-x: 32px; /* minimum space on left and right side of page */ --page-space-bottom: 64px; /* space between last page element and footer */ + + /* z-index */ + --z-index-modal: 1001; /* modal dialog, hard-coded from Fomantic modal.css */ + --z-index-toast: 1002; /* should be larger than modal */ } @media (min-width: 768px) and (max-width: 1200px) { diff --git a/web_src/css/markup/content.css b/web_src/css/markup/content.css index 8f92a51749..c6a89edf25 100644 --- a/web_src/css/markup/content.css +++ b/web_src/css/markup/content.css @@ -134,7 +134,9 @@ margin-bottom: 16px; } -/* override p:last-child from base.css */ +/* override p:last-child from base.css. +Fomantic assumes that <p>/<hX> elements only have margins between elements, but not for the first's top or last's bottom. +In markup content, we always use bottom margin for all elements */ .markup p:last-child { margin-bottom: 16px; } diff --git a/web_src/css/modules/breadcrumb.css b/web_src/css/modules/breadcrumb.css index ca488c2150..77e31ef627 100644 --- a/web_src/css/modules/breadcrumb.css +++ b/web_src/css/modules/breadcrumb.css @@ -1,14 +1,10 @@ .breadcrumb { display: flex; - flex-wrap: wrap; align-items: center; gap: 3px; + overflow-wrap: anywhere; } .breadcrumb .breadcrumb-divider { color: var(--color-text-light-2); } - -.breadcrumb > * { - display: inline; -} diff --git a/web_src/css/modules/dimmer.css b/web_src/css/modules/dimmer.css index 8924821370..7d1ca6171a 100644 --- a/web_src/css/modules/dimmer.css +++ b/web_src/css/modules/dimmer.css @@ -20,7 +20,7 @@ opacity: 1; } -.ui.dimmer > * { +.ui.dimmer > .ui.modal { position: static; margin-top: auto !important; margin-bottom: auto !important; diff --git a/web_src/css/modules/toast.css b/web_src/css/modules/toast.css index 1145f3b1b5..330d3b176e 100644 --- a/web_src/css/modules/toast.css +++ b/web_src/css/modules/toast.css @@ -3,7 +3,7 @@ position: fixed; opacity: 0; transition: all .2s ease; - z-index: 500; + z-index: var(--z-index-toast); border-radius: var(--border-radius); box-shadow: 0 8px 24px var(--color-shadow); display: flex; diff --git a/web_src/css/repo.css b/web_src/css/repo.css index cbc890e356..1a05b68dd4 100644 --- a/web_src/css/repo.css +++ b/web_src/css/repo.css @@ -139,11 +139,6 @@ td .commit-summary { } } -.repo-path { - display: flex; - overflow-wrap: anywhere; -} - .repository.file.list .non-diff-file-content .header .icon { font-size: 1em; } @@ -1839,6 +1834,7 @@ tbody.commit-list { border-radius: 0; display: flex; flex-direction: column; + gap: 0.5em; } /* fomantic's last-child selector does not work with hidden last child */ diff --git a/web_src/css/repo/wiki.css b/web_src/css/repo/wiki.css index ca59dadb9c..144cb1206c 100644 --- a/web_src/css/repo/wiki.css +++ b/web_src/css/repo/wiki.css @@ -39,10 +39,6 @@ min-width: 150px; } -.repository.wiki .wiki-content-sidebar .ui.message.unicode-escape-prompt p { - display: none; -} - .repository.wiki .wiki-content-footer { margin-top: 1em; } diff --git a/web_src/fomantic/build/components/dropdown.js b/web_src/fomantic/build/components/dropdown.js index 85530c7991..3ad0984865 100644 --- a/web_src/fomantic/build/components/dropdown.js +++ b/web_src/fomantic/build/components/dropdown.js @@ -525,7 +525,7 @@ $.fn.dropdown = function(parameters) { return true; } if(settings.onShow.call(element) !== false) { - settings.onAfterFiltered.call(element); // GITEA-PATCH: callback to correctly handle the filtered items + $module.fomanticExt.onDropdownAfterFiltered.call(element); // GITEA-PATCH: callback to correctly handle the filtered items module.animate.show(function() { if( module.can.click() ) { module.bind.intent(); @@ -753,7 +753,7 @@ $.fn.dropdown = function(parameters) { if(module.is.searchSelection() && module.can.show() && module.is.focusedOnSearch() ) { module.show(); } - settings.onAfterFiltered.call(element); // GITEA-PATCH: callback to correctly handle the filtered items + $module.fomanticExt.onDropdownAfterFiltered.call(element); // GITEA-PATCH: callback to correctly handle the filtered items } ; if(settings.useLabels && module.has.maxSelections()) { @@ -3994,8 +3994,6 @@ $.fn.dropdown.settings = { onShow : function(){}, onHide : function(){}, - onAfterFiltered: function(){}, // GITEA-PATCH: callback to correctly handle the filtered items - /* Component */ name : 'Dropdown', namespace : 'dropdown', diff --git a/web_src/fomantic/build/components/modal.js b/web_src/fomantic/build/components/modal.js index 420ecc250b..3f578ccfcc 100644 --- a/web_src/fomantic/build/components/modal.js +++ b/web_src/fomantic/build/components/modal.js @@ -467,7 +467,7 @@ $.fn.modal = function(parameters) { ignoreRepeatedEvents = false; return false; } - + $module.fomanticExt.onModalBeforeHidden.call(element); // GITEA-PATCH: handle more UI updates before hidden if( module.is.animating() || module.is.active() ) { if(settings.transition && $.fn.transition !== undefined && $module.transition('is supported')) { module.remove.active(); @@ -641,7 +641,7 @@ $.fn.modal = function(parameters) { $module .off('mousedown' + elementEventNamespace) ; - } + } $dimmer .off('mousedown' + elementEventNamespace) ; @@ -877,7 +877,7 @@ $.fn.modal = function(parameters) { ? $(document).scrollTop() + settings.padding : $(document).scrollTop() + (module.cache.contextHeight - module.cache.height - settings.padding), marginLeft: -(module.cache.width / 2) - }) + }) ; } else { $module @@ -886,7 +886,7 @@ $.fn.modal = function(parameters) { ? -(module.cache.height / 2) : settings.padding / 2, marginLeft: -(module.cache.width / 2) - }) + }) ; } module.verbose('Setting modal offset for legacy mode'); diff --git a/web_src/js/components/ActivityHeatmap.vue b/web_src/js/components/ActivityHeatmap.vue index eaa9b0ffb1..296cb61cff 100644 --- a/web_src/js/components/ActivityHeatmap.vue +++ b/web_src/js/components/ActivityHeatmap.vue @@ -1,7 +1,7 @@ <script lang="ts" setup> // TODO: Switch to upstream after https://github.com/razorness/vue3-calendar-heatmap/pull/34 is merged import {CalendarHeatmap} from '@silverwind/vue3-calendar-heatmap'; -import {onMounted, ref} from 'vue'; +import {onMounted, shallowRef} from 'vue'; import type {Value as HeatmapValue, Locale as HeatmapLocale} from '@silverwind/vue3-calendar-heatmap'; defineProps<{ @@ -24,7 +24,7 @@ const colorRange = [ 'var(--color-primary-dark-4)', ]; -const endDate = ref(new Date()); +const endDate = shallowRef(new Date()); onMounted(() => { // work around issue with first legend color being rendered twice and legend cut off diff --git a/web_src/js/components/ContextPopup.vue b/web_src/js/components/ContextPopup.vue index 0aae202d42..5ec4499e48 100644 --- a/web_src/js/components/ContextPopup.vue +++ b/web_src/js/components/ContextPopup.vue @@ -2,16 +2,16 @@ import {SvgIcon} from '../svg.ts'; import {GET} from '../modules/fetch.ts'; import {getIssueColor, getIssueIcon} from '../features/issue.ts'; -import {computed, onMounted, ref} from 'vue'; +import {computed, onMounted, shallowRef, useTemplateRef} from 'vue'; import type {IssuePathInfo} from '../types.ts'; const {appSubUrl, i18n} = window.config; -const loading = ref(false); -const issue = ref(null); -const renderedLabels = ref(''); +const loading = shallowRef(false); +const issue = shallowRef(null); +const renderedLabels = shallowRef(''); const i18nErrorOccurred = i18n.error_occurred; -const i18nErrorMessage = ref(null); +const i18nErrorMessage = shallowRef(null); const createdAt = computed(() => new Date(issue.value.created_at).toLocaleDateString(undefined, {year: 'numeric', month: 'short', day: 'numeric'})); const body = computed(() => { @@ -22,7 +22,7 @@ const body = computed(() => { return body; }); -const root = ref<HTMLElement | null>(null); +const root = useTemplateRef('root'); onMounted(() => { root.value.addEventListener('ce-load-context-popup', (e: CustomEventInit<IssuePathInfo>) => { diff --git a/web_src/js/components/DiffFileTreeItem.vue b/web_src/js/components/DiffFileTreeItem.vue index 24bf590082..f15f093ff8 100644 --- a/web_src/js/components/DiffFileTreeItem.vue +++ b/web_src/js/components/DiffFileTreeItem.vue @@ -1,6 +1,6 @@ <script lang="ts" setup> import {SvgIcon, type SvgName} from '../svg.ts'; -import {ref} from 'vue'; +import {shallowRef} from 'vue'; import {type DiffStatus, type DiffTreeEntry, diffTreeStore} from '../modules/diff-file.ts'; const props = defineProps<{ @@ -8,7 +8,7 @@ const props = defineProps<{ }>(); const store = diffTreeStore(); -const collapsed = ref(props.item.IsViewed); +const collapsed = shallowRef(props.item.IsViewed); function getIconForDiffStatus(pType: DiffStatus) { const diffTypes: Record<DiffStatus, { name: SvgName, classes: Array<string> }> = { diff --git a/web_src/js/components/PullRequestMergeForm.vue b/web_src/js/components/PullRequestMergeForm.vue index 4f291f5ca1..b2c28414c0 100644 --- a/web_src/js/components/PullRequestMergeForm.vue +++ b/web_src/js/components/PullRequestMergeForm.vue @@ -1,19 +1,19 @@ <script lang="ts" setup> -import {computed, onMounted, onUnmounted, ref, watch} from 'vue'; +import {computed, onMounted, onUnmounted, shallowRef, watch} from 'vue'; import {SvgIcon} from '../svg.ts'; import {toggleElem} from '../utils/dom.ts'; const {csrfToken, pageData} = window.config; -const mergeForm = ref(pageData.pullRequestMergeForm); +const mergeForm = pageData.pullRequestMergeForm; -const mergeTitleFieldValue = ref(''); -const mergeMessageFieldValue = ref(''); -const deleteBranchAfterMerge = ref(false); -const autoMergeWhenSucceed = ref(false); +const mergeTitleFieldValue = shallowRef(''); +const mergeMessageFieldValue = shallowRef(''); +const deleteBranchAfterMerge = shallowRef(false); +const autoMergeWhenSucceed = shallowRef(false); -const mergeStyle = ref(''); -const mergeStyleDetail = ref({ +const mergeStyle = shallowRef(''); +const mergeStyleDetail = shallowRef({ hideMergeMessageTexts: false, textDoMerge: '', mergeTitleFieldText: '', @@ -21,33 +21,33 @@ const mergeStyleDetail = ref({ hideAutoMerge: false, }); -const mergeStyleAllowedCount = ref(0); +const mergeStyleAllowedCount = shallowRef(0); -const showMergeStyleMenu = ref(false); -const showActionForm = ref(false); +const showMergeStyleMenu = shallowRef(false); +const showActionForm = shallowRef(false); const mergeButtonStyleClass = computed(() => { - if (mergeForm.value.allOverridableChecksOk) return 'primary'; + if (mergeForm.allOverridableChecksOk) return 'primary'; return autoMergeWhenSucceed.value ? 'primary' : 'red'; }); const forceMerge = computed(() => { - return mergeForm.value.canMergeNow && !mergeForm.value.allOverridableChecksOk; + return mergeForm.canMergeNow && !mergeForm.allOverridableChecksOk; }); watch(mergeStyle, (val) => { - mergeStyleDetail.value = mergeForm.value.mergeStyles.find((e: any) => e.name === val); + mergeStyleDetail.value = mergeForm.mergeStyles.find((e: any) => e.name === val); for (const elem of document.querySelectorAll('[data-pull-merge-style]')) { toggleElem(elem, elem.getAttribute('data-pull-merge-style') === val); } }); onMounted(() => { - mergeStyleAllowedCount.value = mergeForm.value.mergeStyles.reduce((v: any, msd: any) => v + (msd.allowed ? 1 : 0), 0); + mergeStyleAllowedCount.value = mergeForm.mergeStyles.reduce((v: any, msd: any) => v + (msd.allowed ? 1 : 0), 0); - let mergeStyle = mergeForm.value.mergeStyles.find((e: any) => e.allowed && e.name === mergeForm.value.defaultMergeStyle)?.name; - if (!mergeStyle) mergeStyle = mergeForm.value.mergeStyles.find((e: any) => e.allowed)?.name; - switchMergeStyle(mergeStyle, !mergeForm.value.canMergeNow); + let mergeStyle = mergeForm.mergeStyles.find((e: any) => e.allowed && e.name === mergeForm.defaultMergeStyle)?.name; + if (!mergeStyle) mergeStyle = mergeForm.mergeStyles.find((e: any) => e.allowed)?.name; + switchMergeStyle(mergeStyle, !mergeForm.canMergeNow); document.addEventListener('mouseup', hideMergeStyleMenu); }); @@ -63,7 +63,7 @@ function hideMergeStyleMenu() { function toggleActionForm(show: boolean) { showActionForm.value = show; if (!show) return; - deleteBranchAfterMerge.value = mergeForm.value.defaultDeleteBranchAfterMerge; + deleteBranchAfterMerge.value = mergeForm.defaultDeleteBranchAfterMerge; mergeTitleFieldValue.value = mergeStyleDetail.value.mergeTitleFieldText; mergeMessageFieldValue.value = mergeStyleDetail.value.mergeMessageFieldText; } @@ -74,7 +74,7 @@ function switchMergeStyle(name: string, autoMerge = false) { } function clearMergeMessage() { - mergeMessageFieldValue.value = mergeForm.value.defaultMergeMessage; + mergeMessageFieldValue.value = mergeForm.defaultMergeMessage; } </script> diff --git a/web_src/js/components/RepoActivityTopAuthors.vue b/web_src/js/components/RepoActivityTopAuthors.vue index 77b85bd7e2..bbdfda41d0 100644 --- a/web_src/js/components/RepoActivityTopAuthors.vue +++ b/web_src/js/components/RepoActivityTopAuthors.vue @@ -1,9 +1,9 @@ <script lang="ts" setup> // @ts-expect-error - module exports no types import {VueBarGraph} from 'vue-bar-graph'; -import {computed, onMounted, ref} from 'vue'; +import {computed, onMounted, shallowRef, useTemplateRef} from 'vue'; -const colors = ref({ +const colors = shallowRef({ barColor: 'green', textColor: 'black', textAltColor: 'white', @@ -41,8 +41,8 @@ const graphWidth = computed(() => { return activityTopAuthors.length * 40; }); -const styleElement = ref<HTMLElement | null>(null); -const altStyleElement = ref<HTMLElement | null>(null); +const styleElement = useTemplateRef('styleElement'); +const altStyleElement = useTemplateRef('altStyleElement'); onMounted(() => { const refStyle = window.getComputedStyle(styleElement.value); diff --git a/web_src/js/components/RepoCodeFrequency.vue b/web_src/js/components/RepoCodeFrequency.vue index f04fc065b6..f331a26fe9 100644 --- a/web_src/js/components/RepoCodeFrequency.vue +++ b/web_src/js/components/RepoCodeFrequency.vue @@ -23,7 +23,7 @@ import { import {chartJsColors} from '../utils/color.ts'; import {sleep} from '../utils.ts'; import 'chartjs-adapter-dayjs-4/dist/chartjs-adapter-dayjs-4.esm'; -import {onMounted, ref} from 'vue'; +import {onMounted, shallowRef} from 'vue'; const {pageData} = window.config; @@ -47,10 +47,10 @@ defineProps<{ }; }>(); -const isLoading = ref(false); -const errorText = ref(''); -const repoLink = ref(pageData.repoLink || []); -const data = ref<DayData[]>([]); +const isLoading = shallowRef(false); +const errorText = shallowRef(''); +const repoLink = pageData.repoLink; +const data = shallowRef<DayData[]>([]); onMounted(() => { fetchGraphData(); @@ -61,7 +61,7 @@ async function fetchGraphData() { try { let response: Response; do { - response = await GET(`${repoLink.value}/activity/code-frequency/data`); + response = await GET(`${repoLink}/activity/code-frequency/data`); if (response.status === 202) { await sleep(1000); // wait for 1 second before retrying } diff --git a/web_src/js/components/RepoContributors.vue b/web_src/js/components/RepoContributors.vue index 1006ea30bb..754acb997d 100644 --- a/web_src/js/components/RepoContributors.vue +++ b/web_src/js/components/RepoContributors.vue @@ -397,7 +397,7 @@ export default defineComponent({ <div class="ui top attached header tw-flex tw-flex-1"> <b class="ui right">#{{ index + 1 }}</b> <a :href="contributor.home_link"> - <img class="ui avatar tw-align-middle" height="40" width="40" :src="contributor.avatar_link" alt=""> + <img loading="lazy" class="ui avatar tw-align-middle" height="40" width="40" :src="contributor.avatar_link" alt=""> </a> <div class="tw-ml-2"> <a v-if="contributor.home_link !== ''" :href="contributor.home_link"><h4>{{ contributor.name }}</h4></a> diff --git a/web_src/js/components/RepoRecentCommits.vue b/web_src/js/components/RepoRecentCommits.vue index 1b3d8fd459..27aa27dfc3 100644 --- a/web_src/js/components/RepoRecentCommits.vue +++ b/web_src/js/components/RepoRecentCommits.vue @@ -21,7 +21,7 @@ import { import {chartJsColors} from '../utils/color.ts'; import {sleep} from '../utils.ts'; import 'chartjs-adapter-dayjs-4/dist/chartjs-adapter-dayjs-4.esm'; -import {onMounted, ref} from 'vue'; +import {onMounted, ref, shallowRef} from 'vue'; const {pageData} = window.config; @@ -43,9 +43,9 @@ defineProps<{ }; }>(); -const isLoading = ref(false); -const errorText = ref(''); -const repoLink = ref(pageData.repoLink || []); +const isLoading = shallowRef(false); +const errorText = shallowRef(''); +const repoLink = pageData.repoLink; const data = ref<DayData[]>([]); onMounted(() => { @@ -57,7 +57,7 @@ async function fetchGraphData() { try { let response: Response; do { - response = await GET(`${repoLink.value}/activity/recent-commits/data`); + response = await GET(`${repoLink}/activity/recent-commits/data`); if (response.status === 202) { await sleep(1000); // wait for 1 second before retrying } diff --git a/web_src/js/components/ViewFileTree.vue b/web_src/js/components/ViewFileTree.vue index d560824159..1f90f92586 100644 --- a/web_src/js/components/ViewFileTree.vue +++ b/web_src/js/components/ViewFileTree.vue @@ -1,9 +1,9 @@ <script lang="ts" setup> import ViewFileTreeItem from './ViewFileTreeItem.vue'; -import {onMounted, ref} from 'vue'; +import {onMounted, useTemplateRef} from 'vue'; import {createViewFileTreeStore} from './ViewFileTreeStore.ts'; -const elRoot = ref<HTMLElement | null>(null); +const elRoot = useTemplateRef('elRoot'); const props = defineProps({ repoLink: {type: String, required: true}, diff --git a/web_src/js/components/ViewFileTreeItem.vue b/web_src/js/components/ViewFileTreeItem.vue index 4a7569e921..5173c7eb46 100644 --- a/web_src/js/components/ViewFileTreeItem.vue +++ b/web_src/js/components/ViewFileTreeItem.vue @@ -1,7 +1,7 @@ <script lang="ts" setup> import {SvgIcon} from '../svg.ts'; import {isPlainClick} from '../utils/dom.ts'; -import {ref} from 'vue'; +import {shallowRef} from 'vue'; import {type createViewFileTreeStore} from './ViewFileTreeStore.ts'; type Item = { @@ -20,9 +20,9 @@ const props = defineProps<{ }>(); const store = props.store; -const isLoading = ref(false); -const children = ref(props.item.children); -const collapsed = ref(!props.item.children); +const isLoading = shallowRef(false); +const children = shallowRef(props.item.children); +const collapsed = shallowRef(!props.item.children); const doLoadChildren = async () => { collapsed.value = !collapsed.value; diff --git a/web_src/js/features/common-fetch-action.ts b/web_src/js/features/common-fetch-action.ts index a4a69540a8..a372216ae6 100644 --- a/web_src/js/features/common-fetch-action.ts +++ b/web_src/js/features/common-fetch-action.ts @@ -1,11 +1,11 @@ import {request} from '../modules/fetch.ts'; -import {showErrorToast} from '../modules/toast.ts'; +import {hideToastsAll, showErrorToast} from '../modules/toast.ts'; import {addDelegatedEventListener, submitEventSubmitter} from '../utils/dom.ts'; import {confirmModal} from './comp/ConfirmModal.ts'; import type {RequestOpts} from '../types.ts'; import {ignoreAreYouSure} from '../vendor/jquery.are-you-sure.ts'; -const {appSubUrl, i18n} = window.config; +const {appSubUrl} = window.config; // fetchActionDoRedirect does real redirection to bypass the browser's limitations of "location" // more details are in the backend's fetch-redirect handler @@ -23,10 +23,20 @@ function fetchActionDoRedirect(redirect: string) { } async function fetchActionDoRequest(actionElem: HTMLElement, url: string, opt: RequestOpts) { + const showErrorForResponse = (code: number, message: string) => { + showErrorToast(`Error ${code || 'request'}: ${message}`); + }; + + let respStatus = 0; + let respText = ''; try { + hideToastsAll(); const resp = await request(url, opt); - if (resp.status === 200) { - let {redirect} = await resp.json(); + respStatus = resp.status; + respText = await resp.text(); + const respJson = JSON.parse(respText); + if (respStatus === 200) { + let {redirect} = respJson; redirect = redirect || actionElem.getAttribute('data-redirect'); ignoreAreYouSure(actionElem); // ignore the areYouSure check before reloading if (redirect) { @@ -35,22 +45,21 @@ async function fetchActionDoRequest(actionElem: HTMLElement, url: string, opt: R window.location.reload(); } return; - } else if (resp.status >= 400 && resp.status < 500) { - const data = await resp.json(); + } + + if (respStatus >= 400 && respStatus < 500 && respJson?.errorMessage) { // the code was quite messy, sometimes the backend uses "err", sometimes it uses "error", and even "user_error" // but at the moment, as a new approach, we only use "errorMessage" here, backend can use JSONError() to respond. - if (data.errorMessage) { - showErrorToast(data.errorMessage, {useHtmlBody: data.renderFormat === 'html'}); - } else { - showErrorToast(`server error: ${resp.status}`); - } + showErrorToast(respJson.errorMessage, {useHtmlBody: respJson.renderFormat === 'html'}); } else { - showErrorToast(`server error: ${resp.status}`); + showErrorForResponse(respStatus, respText); } } catch (e) { - if (e.name !== 'AbortError') { - console.error('error when doRequest', e); - showErrorToast(`${i18n.network_error} ${e}`); + if (e.name === 'SyntaxError') { + showErrorForResponse(respStatus, (respText || '').substring(0, 100)); + } else if (e.name !== 'AbortError') { + console.error('fetchActionDoRequest error', e); + showErrorForResponse(respStatus, `${e}`); } } actionElem.classList.remove('is-loading', 'loading-icon-2px'); @@ -70,7 +79,7 @@ export async function submitFormFetchAction(formEl: HTMLFormElement, formSubmitt } const formMethod = formEl.getAttribute('method') || 'get'; - const formActionUrl = formEl.getAttribute('action'); + const formActionUrl = formEl.getAttribute('action') || window.location.href; const formData = new FormData(formEl); const [submitterName, submitterValue] = [formSubmitter?.getAttribute('name'), formSubmitter?.getAttribute('value')]; if (submitterName) { diff --git a/web_src/js/features/install.ts b/web_src/js/features/install.ts index 34df4757f9..ca4bcce881 100644 --- a/web_src/js/features/install.ts +++ b/web_src/js/features/install.ts @@ -104,7 +104,7 @@ function initPreInstall() { } function initPostInstall() { - const el = document.querySelector('#goto-user-login'); + const el = document.querySelector('#goto-after-install'); if (!el) return; const targetUrl = el.getAttribute('href'); diff --git a/web_src/js/features/repo-editor.ts b/web_src/js/features/repo-editor.ts index acf4127399..c6b5cccd54 100644 --- a/web_src/js/features/repo-editor.ts +++ b/web_src/js/features/repo-editor.ts @@ -7,6 +7,7 @@ import {initDropzone} from './dropzone.ts'; import {confirmModal} from './comp/ConfirmModal.ts'; import {applyAreYouSure, ignoreAreYouSure} from '../vendor/jquery.are-you-sure.ts'; import {fomanticQuery} from '../modules/fomantic/base.ts'; +import {submitFormFetchAction} from './common-fetch-action.ts'; function initEditPreviewTab(elForm: HTMLFormElement) { const elTabMenu = elForm.querySelector('.repo-editor-menu'); @@ -143,31 +144,28 @@ export function initRepoEditor() { const elForm = document.querySelector<HTMLFormElement>('.repository.editor .edit.form'); + // on the upload page, there is no editor(textarea) + const editArea = document.querySelector<HTMLTextAreaElement>('.page-content.repository.editor textarea#edit_area'); + if (!editArea) return; + // Using events from https://github.com/codedance/jquery.AreYouSure#advanced-usage // to enable or disable the commit button const commitButton = document.querySelector<HTMLButtonElement>('#commit-button'); const dirtyFileClass = 'dirty-file'; - // Enabling the button at the start if the page has posted - if (document.querySelector<HTMLInputElement>('input[name="page_has_posted"]')?.value === 'true') { - commitButton.disabled = false; - } - + const syncCommitButtonState = () => { + const dirty = elForm.classList.contains(dirtyFileClass); + commitButton.disabled = !dirty; + }; // Registering a custom listener for the file path and the file content // FIXME: it is not quite right here (old bug), it causes double-init, the global areYouSure "dirty" class will also be added applyAreYouSure(elForm, { silent: true, dirtyClass: dirtyFileClass, fieldSelector: ':input:not(.commit-form-wrapper :input)', - change($form: any) { - const dirty = $form[0]?.classList.contains(dirtyFileClass); - commitButton.disabled = !dirty; - }, + change: syncCommitButtonState, }); - - // on the upload page, there is no editor(textarea) - const editArea = document.querySelector<HTMLTextAreaElement>('.page-content.repository.editor textarea#edit_area'); - if (!editArea) return; + syncCommitButtonState(); // disable the "commit" button when no content changes initEditPreviewTab(elForm); @@ -182,7 +180,7 @@ export function initRepoEditor() { editor.setValue(value); } - commitButton?.addEventListener('click', async (e) => { + commitButton.addEventListener('click', async (e) => { // A modal which asks if an empty file should be committed if (!editArea.value) { e.preventDefault(); @@ -191,7 +189,7 @@ export function initRepoEditor() { content: elForm.getAttribute('data-text-empty-confirm-content'), })) { ignoreAreYouSure(elForm); - elForm.submit(); + submitFormFetchAction(elForm); } } }); diff --git a/web_src/js/modules/fomantic/dropdown.ts b/web_src/js/modules/fomantic/dropdown.ts index 02fee5a267..ccc22073d7 100644 --- a/web_src/js/modules/fomantic/dropdown.ts +++ b/web_src/js/modules/fomantic/dropdown.ts @@ -9,9 +9,9 @@ const fomanticDropdownFn = $.fn.dropdown; // use our own `$().dropdown` function to patch Fomantic's dropdown module export function initAriaDropdownPatch() { if ($.fn.dropdown === ariaDropdownFn) throw new Error('initAriaDropdownPatch could only be called once'); - $.fn.dropdown.settings.onAfterFiltered = onAfterFiltered; $.fn.dropdown = ariaDropdownFn; $.fn.fomanticExt.onResponseKeepSelectedItem = onResponseKeepSelectedItem; + $.fn.fomanticExt.onDropdownAfterFiltered = onDropdownAfterFiltered; (ariaDropdownFn as FomanticInitFunction).settings = fomanticDropdownFn.settings; } @@ -71,7 +71,7 @@ function updateSelectionLabel(label: HTMLElement) { } } -function onAfterFiltered(this: any) { +function onDropdownAfterFiltered(this: any) { const $dropdown = $(this).closest('.ui.dropdown'); // "this" can be the "ui dropdown" or "<select>" const hideEmptyDividers = $dropdown.dropdown('setting', 'hideDividers') === 'empty'; const itemsMenu = $dropdown[0].querySelector('.scrolling.menu') || $dropdown[0].querySelector('.menu'); diff --git a/web_src/js/modules/fomantic/modal.ts b/web_src/js/modules/fomantic/modal.ts index 6a2c558890..b07b941590 100644 --- a/web_src/js/modules/fomantic/modal.ts +++ b/web_src/js/modules/fomantic/modal.ts @@ -1,5 +1,7 @@ import $ from 'jquery'; import type {FomanticInitFunction} from '../../types.ts'; +import {queryElems} from '../../utils/dom.ts'; +import {hideToastsFrom} from '../toast.ts'; const fomanticModalFn = $.fn.modal; @@ -7,6 +9,7 @@ const fomanticModalFn = $.fn.modal; export function initAriaModalPatch() { if ($.fn.modal === ariaModalFn) throw new Error('initAriaModalPatch could only be called once'); $.fn.modal = ariaModalFn; + $.fn.fomanticExt.onModalBeforeHidden = onModalBeforeHidden; (ariaModalFn as FomanticInitFunction).settings = fomanticModalFn.settings; } @@ -27,3 +30,10 @@ function ariaModalFn(this: any, ...args: Parameters<FomanticInitFunction>) { } return ret; } + +function onModalBeforeHidden(this: any) { + const $modal = $(this); + const elModal = $modal[0]; + queryElems(elModal, 'form', (form: HTMLFormElement) => form.reset()); + hideToastsFrom(elModal.closest('.ui.dimmer') ?? document.body); +} diff --git a/web_src/js/modules/toast.ts b/web_src/js/modules/toast.ts index 36e2321743..b0afc343c3 100644 --- a/web_src/js/modules/toast.ts +++ b/web_src/js/modules/toast.ts @@ -1,6 +1,6 @@ import {htmlEscape} from 'escape-goat'; import {svg} from '../svg.ts'; -import {animateOnce, showElem} from '../utils/dom.ts'; +import {animateOnce, queryElems, showElem} from '../utils/dom.ts'; import Toastify from 'toastify-js'; // don't use "async import", because when network error occurs, the "async import" also fails and nothing is shown import type {Intent} from '../types.ts'; import type {SvgName} from '../svg.ts'; @@ -37,17 +37,20 @@ const levels: ToastLevels = { type ToastOpts = { useHtmlBody?: boolean, - preventDuplicates?: boolean, + preventDuplicates?: boolean | string, } & Options; +type ToastifyElement = HTMLElement & {_giteaToastifyInstance?: Toast }; + // See https://github.com/apvarun/toastify-js#api for options function showToast(message: string, level: Intent, {gravity, position, duration, useHtmlBody, preventDuplicates = true, ...other}: ToastOpts = {}): Toast { const body = useHtmlBody ? String(message) : htmlEscape(message); - const key = `${level}-${body}`; + const parent = document.querySelector('.ui.dimmer.active') ?? document.body; + const duplicateKey = preventDuplicates ? (preventDuplicates === true ? `${level}-${body}` : preventDuplicates) : ''; - // prevent showing duplicate toasts with same level and message, and give a visual feedback for end users + // prevent showing duplicate toasts with the same level and message, and give visual feedback for end users if (preventDuplicates) { - const toastEl = document.querySelector(`.toastify[data-toast-unique-key="${CSS.escape(key)}"]`); + const toastEl = parent.querySelector(`:scope > .toastify.on[data-toast-unique-key="${CSS.escape(duplicateKey)}"]`); if (toastEl) { const toastDupNumEl = toastEl.querySelector('.toast-duplicate-number'); showElem(toastDupNumEl); @@ -59,6 +62,7 @@ function showToast(message: string, level: Intent, {gravity, position, duration, const {icon, background, duration: levelDuration} = levels[level ?? 'info']; const toast = Toastify({ + selector: parent, text: ` <div class='toast-icon'>${svg(icon)}</div> <div class='toast-body'><span class="toast-duplicate-number tw-hidden">1</span>${body}</div> @@ -74,7 +78,8 @@ function showToast(message: string, level: Intent, {gravity, position, duration, toast.showToast(); toast.toastElement.querySelector('.toast-close').addEventListener('click', () => toast.hideToast()); - toast.toastElement.setAttribute('data-toast-unique-key', key); + toast.toastElement.setAttribute('data-toast-unique-key', duplicateKey); + (toast.toastElement as ToastifyElement)._giteaToastifyInstance = toast; return toast; } @@ -89,3 +94,15 @@ export function showWarningToast(message: string, opts?: ToastOpts): Toast { export function showErrorToast(message: string, opts?: ToastOpts): Toast { return showToast(message, 'error', opts); } + +function hideToastByElement(el: Element): void { + (el as ToastifyElement)?._giteaToastifyInstance?.hideToast(); +} + +export function hideToastsFrom(parent: Element): void { + queryElems(parent, ':scope > .toastify.on', hideToastByElement); +} + +export function hideToastsAll(): void { + queryElems(document, '.toastify.on', hideToastByElement); +} |