mirror of
https://github.com/go-gitea/gitea.git
synced 2026-09-22 02:49:32 +02:00
Merge branch 'main' into main
This commit is contained in:
@@ -38,7 +38,7 @@ jobs:
|
||||
contents: read
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: astral-sh/setup-uv@v7
|
||||
- uses: astral-sh/setup-uv@v8.0.0
|
||||
- run: uv python install 3.14
|
||||
- uses: pnpm/action-setup@v5
|
||||
- uses: actions/setup-node@v6
|
||||
@@ -58,7 +58,7 @@ jobs:
|
||||
contents: read
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: astral-sh/setup-uv@v7
|
||||
- uses: astral-sh/setup-uv@v8.0.0
|
||||
- run: uv python install 3.14
|
||||
- run: make deps-py
|
||||
- run: make lint-yaml
|
||||
|
||||
@@ -525,7 +525,7 @@ test-mssql-migration: migrations.mssql.test migrations.individual.mssql.test
|
||||
.PHONY: playwright
|
||||
playwright: deps-frontend
|
||||
@# on GitHub Actions VMs, playwright's system deps are pre-installed
|
||||
@pnpm exec playwright install $(if $(GITHUB_ACTIONS),,--with-deps) chromium $(if $(CI),firefox) $(PLAYWRIGHT_FLAGS)
|
||||
@pnpm exec playwright install $(if $(GITHUB_ACTIONS),,--with-deps) chromium firefox $(PLAYWRIGHT_FLAGS)
|
||||
|
||||
.PHONY: test-e2e
|
||||
test-e2e: playwright $(EXECUTABLE_E2E)
|
||||
|
||||
+1
-5
@@ -78,11 +78,7 @@ func runGenerateInternalToken(_ context.Context, c *cli.Command) error {
|
||||
}
|
||||
|
||||
func runGenerateLfsJwtSecret(_ context.Context, c *cli.Command) error {
|
||||
_, jwtSecretBase64, err := generate.NewJwtSecretWithBase64()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, jwtSecretBase64 := generate.NewJwtSecretWithBase64()
|
||||
fmt.Printf("%s", jwtSecretBase64)
|
||||
|
||||
if isatty.IsTerminal(os.Stdout.Fd()) {
|
||||
|
||||
@@ -54,13 +54,13 @@ func DecodeJwtSecretBase64(src string) ([]byte, error) {
|
||||
}
|
||||
|
||||
// NewJwtSecretWithBase64 generates a jwt secret with its base64 encoded value intended to be used for saving into config file
|
||||
func NewJwtSecretWithBase64() ([]byte, string, error) {
|
||||
func NewJwtSecretWithBase64() ([]byte, string) {
|
||||
bytes := make([]byte, defaultJwtSecretLen)
|
||||
_, err := io.ReadFull(rand.Reader, bytes)
|
||||
_, err := rand.Read(bytes)
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
panic(err) // rand.Read never fails
|
||||
}
|
||||
return bytes, base64.RawURLEncoding.EncodeToString(bytes), nil
|
||||
return bytes, base64.RawURLEncoding.EncodeToString(bytes)
|
||||
}
|
||||
|
||||
// NewSecretKey generate a new value intended to be used by SECRET_KEY.
|
||||
|
||||
@@ -25,10 +25,12 @@ func TestDecodeJwtSecretBase64(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestNewJwtSecretWithBase64(t *testing.T) {
|
||||
secret, encoded, err := NewJwtSecretWithBase64()
|
||||
assert.NoError(t, err)
|
||||
secret, encoded := NewJwtSecretWithBase64()
|
||||
assert.Len(t, secret, 32)
|
||||
decoded, err := DecodeJwtSecretBase64(encoded)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, secret, decoded)
|
||||
|
||||
secret2, _ := NewJwtSecretWithBase64()
|
||||
assert.NotEqual(t, secret, secret2)
|
||||
}
|
||||
|
||||
@@ -583,3 +583,20 @@ func TestMarkdownLink(t *testing.T) {
|
||||
assert.Equal(t, `<p><a href="https://example.com/__init__.py" rel="nofollow">https://example.com/__init__.py</a></p>
|
||||
`, string(result))
|
||||
}
|
||||
|
||||
func TestMarkdownUlDir(t *testing.T) {
|
||||
defer test.MockVariableValue(&markup.RenderBehaviorForTesting.DisableAdditionalAttributes, false)()
|
||||
result, err := markdown.RenderString(markup.NewTestRenderContext(), `
|
||||
* a
|
||||
* b
|
||||
`)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, `<ul dir="auto">
|
||||
<li>a
|
||||
<ul>
|
||||
<li>b</li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
`, string(result))
|
||||
}
|
||||
|
||||
@@ -81,5 +81,16 @@ func (g *ASTTransformer) transformList(_ *markup.RenderContext, v *ast.List, rc
|
||||
v.AppendChild(v, newChild)
|
||||
}
|
||||
}
|
||||
g.applyElementDir(v)
|
||||
|
||||
nestedList := false
|
||||
for p := v.Parent(); p != nil; p = p.Parent() {
|
||||
if _, ok := p.(*ast.List); ok {
|
||||
nestedList = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !nestedList {
|
||||
// "dir=auto" should be only added to top-level "ul". https://github.com/go-gitea/gitea/issues/35058
|
||||
g.applyElementDir(v)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -81,10 +81,7 @@ func loadLFSFrom(rootCfg ConfigProvider) error {
|
||||
jwtSecretBase64 := loadSecret(rootCfg.Section("server"), "LFS_JWT_SECRET_URI", "LFS_JWT_SECRET")
|
||||
LFS.JWTSecretBytes, err = generate.DecodeJwtSecretBase64(jwtSecretBase64)
|
||||
if err != nil {
|
||||
LFS.JWTSecretBytes, jwtSecretBase64, err = generate.NewJwtSecretWithBase64()
|
||||
if err != nil {
|
||||
return fmt.Errorf("error generating JWT Secret for custom config: %v", err)
|
||||
}
|
||||
LFS.JWTSecretBytes, jwtSecretBase64 = generate.NewJwtSecretWithBase64()
|
||||
|
||||
// Save secret
|
||||
saveCfg, err := rootCfg.PrepareSaving()
|
||||
|
||||
@@ -139,10 +139,7 @@ func loadOAuth2From(rootCfg ConfigProvider) {
|
||||
if InstallLock {
|
||||
jwtSecretBytes, err := generate.DecodeJwtSecretBase64(jwtSecretBase64)
|
||||
if err != nil {
|
||||
jwtSecretBytes, jwtSecretBase64, err = generate.NewJwtSecretWithBase64()
|
||||
if err != nil {
|
||||
log.Fatal("error generating JWT secret: %v", err)
|
||||
}
|
||||
jwtSecretBytes, jwtSecretBase64 = generate.NewJwtSecretWithBase64()
|
||||
saveCfg, err := rootCfg.PrepareSaving()
|
||||
if err != nil {
|
||||
log.Fatal("save oauth2.JWT_SECRET failed: %v", err)
|
||||
@@ -162,10 +159,7 @@ var generalSigningSecret atomic.Pointer[[]byte]
|
||||
func GetGeneralTokenSigningSecret() []byte {
|
||||
old := generalSigningSecret.Load()
|
||||
if old == nil || len(*old) == 0 {
|
||||
jwtSecret, _, err := generate.NewJwtSecretWithBase64()
|
||||
if err != nil {
|
||||
log.Fatal("Unable to generate general JWT secret: %v", err)
|
||||
}
|
||||
jwtSecret, _ := generate.NewJwtSecretWithBase64()
|
||||
if generalSigningSecret.CompareAndSwap(old, &jwtSecret) {
|
||||
return jwtSecret
|
||||
}
|
||||
|
||||
@@ -25,8 +25,7 @@ import (
|
||||
"code.gitea.io/gitea/services/gitdiff"
|
||||
)
|
||||
|
||||
// NewFuncMap returns functions for injecting to templates
|
||||
func NewFuncMap() template.FuncMap {
|
||||
func newFuncMapWebPage() template.FuncMap {
|
||||
return map[string]any{
|
||||
"DumpVar": dumpVar,
|
||||
"NIL": func() any { return nil },
|
||||
@@ -40,7 +39,6 @@ func NewFuncMap() template.FuncMap {
|
||||
"QueryEscape": queryEscape,
|
||||
"QueryBuild": QueryBuild,
|
||||
"SanitizeHTML": SanitizeHTML,
|
||||
"DotEscape": dotEscape,
|
||||
|
||||
"PathEscape": url.PathEscape,
|
||||
"PathEscapeSegments": util.PathEscapeSegments,
|
||||
@@ -61,6 +59,7 @@ func NewFuncMap() template.FuncMap {
|
||||
|
||||
// -----------------------------------------------------------------
|
||||
// time / number / format
|
||||
"ShortSha": base.ShortSha,
|
||||
"FileSize": base.FileSize,
|
||||
"CountFmt": countFmt,
|
||||
"Sec2Hour": util.SecToHours,
|
||||
@@ -73,6 +72,7 @@ func NewFuncMap() template.FuncMap {
|
||||
|
||||
"AssetURI": public.AssetURI,
|
||||
"ScriptImport": scriptImport,
|
||||
|
||||
// -----------------------------------------------------------------
|
||||
// setting
|
||||
"AppName": func() string {
|
||||
@@ -84,17 +84,10 @@ func NewFuncMap() template.FuncMap {
|
||||
"AssetUrlPrefix": func() string {
|
||||
return setting.StaticURLPrefix + "/assets"
|
||||
},
|
||||
"AppUrl": func() string {
|
||||
// The usage of AppUrl should be avoided as much as possible,
|
||||
// because the AppURL(ROOT_URL) may not match user's visiting site and the ROOT_URL in app.ini may be incorrect.
|
||||
// And it's difficult for Gitea to guess absolute URL correctly with zero configuration,
|
||||
// because Gitea doesn't know whether the scheme is HTTP or HTTPS unless the reverse proxy could tell Gitea.
|
||||
return setting.AppURL
|
||||
},
|
||||
"AppVer": func() string {
|
||||
return setting.AppVer
|
||||
},
|
||||
"AppDomain": func() string { // documented in mail-templates.md
|
||||
"AppDomain": func() string { // TODO: helm registry still uses it, need to use current request host in the future
|
||||
return setting.Domain
|
||||
},
|
||||
"ShowFooterTemplateLoadTime": func() bool {
|
||||
@@ -143,7 +136,6 @@ func NewFuncMap() template.FuncMap {
|
||||
|
||||
// -----------------------------------------------------------------
|
||||
// misc (TODO: move them to MiscUtils to avoid bloating the main func map)
|
||||
"ShortSha": base.ShortSha,
|
||||
"ActionContent2Commits": ActionContent2Commits,
|
||||
"IsMultilineCommitMessage": isMultilineCommitMessage,
|
||||
"CommentMustAsDiff": gitdiff.CommentMustAsDiff,
|
||||
@@ -177,11 +169,6 @@ func queryEscape(s string) template.URL {
|
||||
return template.URL(url.QueryEscape(s))
|
||||
}
|
||||
|
||||
// dotEscape wraps a dots in names with ZWJ [U+200D] in order to prevent auto-linkers from detecting these as urls
|
||||
func dotEscape(raw string) string {
|
||||
return strings.ReplaceAll(raw, ".", "\u200d.\u200d")
|
||||
}
|
||||
|
||||
// iif is an "inline-if", similar util.Iif[T] but templates need the non-generic version,
|
||||
// and it could be simply used as "{{iif expr trueVal}}" (omit the falseVal).
|
||||
func iif(condition any, vals ...any) any {
|
||||
|
||||
@@ -6,12 +6,14 @@ package templates
|
||||
import (
|
||||
"html/template"
|
||||
"io"
|
||||
"net/url"
|
||||
"regexp"
|
||||
"slices"
|
||||
"strings"
|
||||
"sync"
|
||||
texttmpl "text/template"
|
||||
|
||||
"code.gitea.io/gitea/modules/base"
|
||||
"code.gitea.io/gitea/modules/graceful"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
@@ -34,6 +36,11 @@ type MailRender struct {
|
||||
mockedBodyTemplates map[string]*template.Template
|
||||
}
|
||||
|
||||
// dotEscape wraps a dots in names with ZWJ [U+200D] in order to prevent auto-linkers from detecting these as urls
|
||||
func dotEscape(raw string) string {
|
||||
return strings.ReplaceAll(raw, ".", "\u200d.\u200d")
|
||||
}
|
||||
|
||||
// mailSubjectTextFuncMap returns functions for injecting to text templates, it's only used for mail subject
|
||||
func mailSubjectTextFuncMap() texttmpl.FuncMap {
|
||||
return texttmpl.FuncMap{
|
||||
@@ -41,6 +48,7 @@ func mailSubjectTextFuncMap() texttmpl.FuncMap {
|
||||
"Eval": evalTokens,
|
||||
|
||||
"EllipsisString": util.EllipsisDisplayString,
|
||||
|
||||
"AppName": func() string {
|
||||
return setting.AppName
|
||||
},
|
||||
@@ -50,6 +58,48 @@ func mailSubjectTextFuncMap() texttmpl.FuncMap {
|
||||
}
|
||||
}
|
||||
|
||||
func mailBodyFuncMap() template.FuncMap {
|
||||
// Some of them are documented in mail-templates.md
|
||||
return template.FuncMap{
|
||||
"DumpVar": dumpVar,
|
||||
"NIL": func() any { return nil },
|
||||
|
||||
// html/template related functions
|
||||
"dict": dict,
|
||||
"Iif": iif,
|
||||
"Eval": evalTokens,
|
||||
"HTMLFormat": htmlFormat,
|
||||
"QueryEscape": queryEscape,
|
||||
"QueryBuild": QueryBuild,
|
||||
"SanitizeHTML": SanitizeHTML,
|
||||
|
||||
"PathEscape": url.PathEscape,
|
||||
"PathEscapeSegments": util.PathEscapeSegments,
|
||||
|
||||
"DotEscape": dotEscape,
|
||||
|
||||
// utils
|
||||
"StringUtils": NewStringUtils,
|
||||
"SliceUtils": NewSliceUtils,
|
||||
"JsonUtils": NewJsonUtils,
|
||||
|
||||
// time / number / format
|
||||
"ShortSha": base.ShortSha,
|
||||
"FileSize": base.FileSize,
|
||||
|
||||
// setting
|
||||
"AppName": func() string {
|
||||
return setting.AppName
|
||||
},
|
||||
"AppUrl": func() string {
|
||||
return setting.AppURL
|
||||
},
|
||||
"AppDomain": func() string {
|
||||
return setting.Domain
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
var mailSubjectSplit = regexp.MustCompile(`(?m)^-{3,}\s*$`)
|
||||
|
||||
func newMailRenderer() (*MailRender, error) {
|
||||
@@ -103,7 +153,7 @@ func newMailRenderer() (*MailRender, error) {
|
||||
return renderer.tmplRenderer.Templates().HasTemplate(name)
|
||||
}
|
||||
|
||||
staticFuncMap := NewFuncMap()
|
||||
staticFuncMap := mailBodyFuncMap()
|
||||
renderer.BodyTemplates.ExecuteTemplate = func(w io.Writer, name string, data any) error {
|
||||
if t, ok := renderer.mockedBodyTemplates[name]; ok {
|
||||
return t.Execute(w, data)
|
||||
@@ -131,7 +181,7 @@ func (r *MailRender) MockTemplate(name, subject, body string) func() {
|
||||
texttmpl.Must(r.SubjectTemplates.New(name).Parse(subject))
|
||||
|
||||
oldBody, hasOldBody := r.mockedBodyTemplates[name]
|
||||
mockFuncMap := NewFuncMap()
|
||||
mockFuncMap := mailBodyFuncMap()
|
||||
r.mockedBodyTemplates[name] = template.Must(template.New(name).Funcs(mockFuncMap).Parse(body))
|
||||
return func() {
|
||||
r.SubjectTemplates = oldSubject
|
||||
|
||||
@@ -24,13 +24,13 @@ type pageRenderer struct {
|
||||
}
|
||||
|
||||
func (r *pageRenderer) funcMap(ctx context.Context) template.FuncMap {
|
||||
pageFuncMap := NewFuncMap()
|
||||
pageFuncMap := newFuncMapWebPage()
|
||||
pageFuncMap["ctx"] = func() any { return ctx }
|
||||
return pageFuncMap
|
||||
}
|
||||
|
||||
func (r *pageRenderer) funcMapDummy() template.FuncMap {
|
||||
dummyFuncMap := NewFuncMap()
|
||||
dummyFuncMap := newFuncMapWebPage()
|
||||
dummyFuncMap["ctx"] = func() any { return nil } // for template compilation only, no context available
|
||||
return dummyFuncMap
|
||||
}
|
||||
|
||||
@@ -1043,6 +1043,7 @@
|
||||
"repo.forks": "Forks",
|
||||
"repo.stars": "Stars",
|
||||
"repo.reactions_more": "and %d more",
|
||||
"repo.reactions": "Reactions",
|
||||
"repo.unit_disabled": "The site administrator has disabled this repository section.",
|
||||
"repo.language_other": "Other",
|
||||
"repo.adopt_search": "Enter username to search for unadopted repositories… (leave blank to find all)",
|
||||
@@ -3223,10 +3224,8 @@
|
||||
"admin.config.server_config": "Server Configuration",
|
||||
"admin.config.app_name": "Site Title",
|
||||
"admin.config.app_ver": "Gitea Version",
|
||||
"admin.config.app_url": "Gitea Base URL",
|
||||
"admin.config.custom_conf": "Configuration File Path",
|
||||
"admin.config.custom_file_root_path": "Custom File Root Path",
|
||||
"admin.config.domain": "Server Domain",
|
||||
"admin.config.disable_router_log": "Disable Router Log",
|
||||
"admin.config.run_user": "Run As Username",
|
||||
"admin.config.run_mode": "Run Mode",
|
||||
|
||||
@@ -5,6 +5,8 @@ const timeoutFactor = Number(env.GITEA_TEST_E2E_TIMEOUT_FACTOR) || 1;
|
||||
const timeout = 5000 * timeoutFactor;
|
||||
|
||||
export default defineConfig({
|
||||
workers: '50%',
|
||||
fullyParallel: true,
|
||||
testDir: './tests/e2e/',
|
||||
outputDir: './tests/e2e-output/',
|
||||
testMatch: /.*\.test\.ts/,
|
||||
@@ -28,11 +30,11 @@ export default defineConfig({
|
||||
permissions: ['clipboard-read', 'clipboard-write'],
|
||||
},
|
||||
},
|
||||
...env.CI ? [{
|
||||
{
|
||||
name: 'firefox',
|
||||
use: {
|
||||
...devices['Desktop Firefox'],
|
||||
},
|
||||
}] : [],
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
@@ -1228,10 +1228,10 @@ func Routes() *web.Router {
|
||||
m.Group("/branch_protections", func() {
|
||||
m.Get("", repo.ListBranchProtections)
|
||||
m.Post("", bind(api.CreateBranchProtectionOption{}), mustNotBeArchived, repo.CreateBranchProtection)
|
||||
m.Group("/{name}", func() {
|
||||
m.Group("/*", func() {
|
||||
m.Get("", repo.GetBranchProtection)
|
||||
m.Patch("", bind(api.EditBranchProtectionOption{}), mustNotBeArchived, repo.EditBranchProtection)
|
||||
m.Delete("", repo.DeleteBranchProtection)
|
||||
m.Delete("", mustNotBeArchived, repo.DeleteBranchProtection)
|
||||
})
|
||||
m.Post("/priority", bind(api.UpdateBranchProtectionPriories{}), mustNotBeArchived, repo.UpdateBranchProtectionPriories)
|
||||
}, reqToken(), reqAdmin())
|
||||
|
||||
@@ -563,7 +563,7 @@ func GetBranchProtection(ctx *context.APIContext) {
|
||||
// "$ref": "#/responses/notFound"
|
||||
|
||||
repo := ctx.Repo.Repository
|
||||
bpName := ctx.PathParam("name")
|
||||
bpName := ctx.PathParam("*")
|
||||
bp, err := git_model.GetProtectedBranchRuleByName(ctx, repo.ID, bpName)
|
||||
if err != nil {
|
||||
ctx.APIErrorInternal(err)
|
||||
@@ -845,7 +845,7 @@ func EditBranchProtection(ctx *context.APIContext) {
|
||||
// "$ref": "#/responses/repoArchivedError"
|
||||
form := web.GetForm(ctx).(*api.EditBranchProtectionOption)
|
||||
repo := ctx.Repo.Repository
|
||||
bpName := ctx.PathParam("name")
|
||||
bpName := ctx.PathParam("*")
|
||||
protectBranch, err := git_model.GetProtectedBranchRuleByName(ctx, repo.ID, bpName)
|
||||
if err != nil {
|
||||
ctx.APIErrorInternal(err)
|
||||
@@ -1168,7 +1168,7 @@ func DeleteBranchProtection(ctx *context.APIContext) {
|
||||
// "$ref": "#/responses/notFound"
|
||||
|
||||
repo := ctx.Repo.Repository
|
||||
bpName := ctx.PathParam("name")
|
||||
bpName := ctx.PathParam("*")
|
||||
bp, err := git_model.GetProtectedBranchRuleByName(ctx, repo.ID, bpName)
|
||||
if err != nil {
|
||||
ctx.APIErrorInternal(err)
|
||||
|
||||
@@ -107,15 +107,18 @@ func GetAnnotatedTag(ctx *context.APIContext) {
|
||||
return
|
||||
}
|
||||
|
||||
if tag, err := ctx.Repo.GitRepo.GetAnnotatedTag(sha); err != nil {
|
||||
tag, err := ctx.Repo.GitRepo.GetAnnotatedTag(sha)
|
||||
if err != nil {
|
||||
ctx.APIError(http.StatusBadRequest, err)
|
||||
} else {
|
||||
commit, err := ctx.Repo.GitRepo.GetTagCommit(tag.Name)
|
||||
if err != nil {
|
||||
ctx.APIError(http.StatusBadRequest, err)
|
||||
}
|
||||
ctx.JSON(http.StatusOK, convert.ToAnnotatedTag(ctx, ctx.Repo.Repository, tag, commit))
|
||||
return
|
||||
}
|
||||
|
||||
commit, err := ctx.Repo.GitRepo.GetTagCommit(tag.Name)
|
||||
if err != nil {
|
||||
ctx.APIError(http.StatusBadRequest, err)
|
||||
return
|
||||
}
|
||||
ctx.JSON(http.StatusOK, convert.ToAnnotatedTag(ctx, ctx.Repo.Repository, tag, commit))
|
||||
}
|
||||
|
||||
// GetTag get the tag of a repository
|
||||
|
||||
@@ -371,12 +371,11 @@ func SubmitInstall(ctx *context.Context) {
|
||||
if form.LFSRootPath != "" {
|
||||
cfg.Section("server").Key("LFS_START_SERVER").SetValue("true")
|
||||
cfg.Section("lfs").Key("PATH").SetValue(form.LFSRootPath)
|
||||
var lfsJwtSecret string
|
||||
if _, lfsJwtSecret, err = generate.NewJwtSecretWithBase64(); err != nil {
|
||||
ctx.RenderWithErrDeprecated(ctx.Tr("install.lfs_jwt_secret_failed", err), tplInstall, &form)
|
||||
return
|
||||
|
||||
if !cfg.Section("server").HasKey("LFS_JWT_SECRET_URI") {
|
||||
_, lfsJwtSecret := generate.NewJwtSecretWithBase64()
|
||||
cfg.Section("server").Key("LFS_JWT_SECRET").SetValue(lfsJwtSecret)
|
||||
}
|
||||
cfg.Section("server").Key("LFS_JWT_SECRET").SetValue(lfsJwtSecret)
|
||||
} else {
|
||||
cfg.Section("server").Key("LFS_START_SERVER").SetValue("false")
|
||||
}
|
||||
@@ -437,11 +436,7 @@ func SubmitInstall(ctx *context.Context) {
|
||||
// FIXME: at the moment, no matter oauth2 is enabled or not, it must generate a "oauth2 JWT_SECRET"
|
||||
// see the "loadOAuth2From" in "setting/oauth2.go"
|
||||
if !cfg.Section("oauth2").HasKey("JWT_SECRET") && !cfg.Section("oauth2").HasKey("JWT_SECRET_URI") {
|
||||
_, jwtSecretBase64, err := generate.NewJwtSecretWithBase64()
|
||||
if err != nil {
|
||||
ctx.RenderWithErrDeprecated(ctx.Tr("install.secret_key_failed", err), tplInstall, &form)
|
||||
return
|
||||
}
|
||||
_, jwtSecretBase64 := generate.NewJwtSecretWithBase64()
|
||||
cfg.Section("oauth2").Key("JWT_SECRET").SetValue(jwtSecretBase64)
|
||||
}
|
||||
|
||||
|
||||
@@ -123,9 +123,7 @@ func Config(ctx *context.Context) {
|
||||
ctx.Data["PageIsAdminConfigSummary"] = true
|
||||
|
||||
ctx.Data["CustomConf"] = setting.CustomConf
|
||||
ctx.Data["AppUrl"] = setting.AppURL
|
||||
ctx.Data["AppBuiltWith"] = setting.AppBuiltWith
|
||||
ctx.Data["Domain"] = setting.Domain
|
||||
ctx.Data["RunUser"] = setting.RunUser
|
||||
ctx.Data["RunMode"] = util.ToTitleCase(setting.RunMode)
|
||||
ctx.Data["GitVersion"] = git.DefaultFeatures().VersionInfo()
|
||||
|
||||
@@ -5,10 +5,13 @@ package context
|
||||
|
||||
import (
|
||||
"context"
|
||||
"html/template"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"code.gitea.io/gitea/modules/httplib"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/modules/web/middleware"
|
||||
"code.gitea.io/gitea/services/webtheme"
|
||||
@@ -69,3 +72,14 @@ func (c TemplateContext) CurrentWebBanner() *setting.WebBannerType {
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// AppFullLink returns a full URL link with AppSubURL for the given app link (no AppSubURL)
|
||||
// If no link is given, it returns the current app full URL with sub-path but without trailing slash (that's why it is not named as AppURL)
|
||||
func (c TemplateContext) AppFullLink(link ...string) template.URL {
|
||||
s := httplib.GuessCurrentAppURL(c.parentContext())
|
||||
s = strings.TrimSuffix(s, "/")
|
||||
if len(link) == 0 {
|
||||
return template.URL(s)
|
||||
}
|
||||
return template.URL(s + strings.TrimPrefix(link[0], "/"))
|
||||
}
|
||||
|
||||
@@ -11,10 +11,6 @@
|
||||
<dd>{{AppVer}}{{.AppBuiltWith}}</dd>
|
||||
<dt>{{ctx.Locale.Tr "admin.config.custom_conf"}}</dt>
|
||||
<dd>{{.CustomConf}}</dd>
|
||||
<dt>{{ctx.Locale.Tr "admin.config.app_url"}}</dt>
|
||||
<dd>{{.AppUrl}}</dd>
|
||||
<dt>{{ctx.Locale.Tr "admin.config.domain"}}</dt>
|
||||
<dd>{{.Domain}}</dd>
|
||||
<dt>{{ctx.Locale.Tr "admin.config.disable_router_log"}}</dt>
|
||||
<dd>{{svg (Iif .DisableRouterLog "octicon-check" "octicon-x")}}</dd>
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
{{end}}
|
||||
{{else if or .PageIsDiff .IsViewFile}}
|
||||
<meta property="og:title" content="{{.Title}}">
|
||||
<meta property="og:url" content="{{AppUrl}}{{.Link}}">
|
||||
<meta property="og:url" content="{{ctx.AppFullLink $.Link}}">
|
||||
{{if and .PageIsDiff .Commit}}
|
||||
{{- $commitMessageParts := StringUtils.Cut .Commit.Message "\n" -}}
|
||||
{{- $commitMessageBody := index $commitMessageParts 1 -}}
|
||||
@@ -41,7 +41,7 @@
|
||||
<meta property="og:title" content="{{AppName}}">
|
||||
<meta property="og:type" content="website">
|
||||
<meta property="og:image" content="{{AssetUrlPrefix}}/img/logo.png">
|
||||
<meta property="og:url" content="{{AppUrl}}">
|
||||
<meta property="og:url" content="{{ctx.AppFullLink}}">
|
||||
<meta property="og:description" content="{{MetaDescription}}">
|
||||
{{end}}
|
||||
<meta property="og:site_name" content="{{AppName}}">
|
||||
|
||||
@@ -7,7 +7,7 @@ If you introduce mistakes in it, Gitea JavaScript code wouldn't run correctly.
|
||||
window.addEventListener('error', function(e) {window._globalHandlerErrors=window._globalHandlerErrors||[]; window._globalHandlerErrors.push(e);});
|
||||
window.addEventListener('unhandledrejection', function(e) {window._globalHandlerErrors=window._globalHandlerErrors||[]; window._globalHandlerErrors.push(e);});
|
||||
window.config = {
|
||||
appUrl: '{{AppUrl}}',
|
||||
appUrl: '{{ctx.AppFullLink "/"}}',
|
||||
appSubUrl: '{{AppSubUrl}}',
|
||||
assetUrlPrefix: '{{AssetUrlPrefix}}',
|
||||
runModeIsProd: {{.RunModeIsProd}},
|
||||
|
||||
@@ -84,12 +84,6 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h1><origin-url></h1>
|
||||
<div><origin-url data-url="test/url"></origin-url></div>
|
||||
<div><origin-url data-url="/test/url"></origin-url></div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h1><overflow-menu></h1>
|
||||
<overflow-menu class="ui secondary pointing tabular borderless menu">
|
||||
|
||||
@@ -4,12 +4,12 @@
|
||||
<div class="ui form">
|
||||
<div class="field">
|
||||
<label>{{svg "octicon-code"}} {{ctx.Locale.Tr "packages.alpine.registry"}}</label>
|
||||
<div class="markup"><pre class="code-block"><code><origin-url data-url="{{AppSubUrl}}/api/packages/{{$.PackageDescriptor.Owner.Name}}/alpine"></origin-url>/$branch/$repository</code></pre></div>
|
||||
<div class="markup"><pre class="code-block"><code>{{ctx.AppFullLink}}/api/packages/{{$.PackageDescriptor.Owner.Name}}/alpine/$branch/$repository</code></pre></div>
|
||||
<p>{{ctx.Locale.Tr "packages.alpine.registry.info"}}</p>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>{{svg "octicon-terminal"}} {{ctx.Locale.Tr "packages.alpine.registry.key"}}</label>
|
||||
<div class="markup"><pre class="code-block"><code>curl -JO <origin-url data-url="{{AppSubUrl}}/api/packages/{{$.PackageDescriptor.Owner.Name}}/alpine/key"></origin-url></code></pre></div>
|
||||
<div class="markup"><pre class="code-block"><code>curl -JO {{ctx.AppFullLink}}/api/packages/{{$.PackageDescriptor.Owner.Name}}/alpine/key</code></pre></div>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>{{svg "octicon-terminal"}} {{ctx.Locale.Tr "packages.alpine.install"}}</label>
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
<div class="markup"><pre class="code-block"><code>{{range $i, $repo := .Repositories}}{{if $i}}
|
||||
{{end}}[{{$repo}}]
|
||||
SigLevel = Optional TrustAll
|
||||
Server = <origin-url data-url="{{AppSubUrl}}/api/packages/{{$.PackageDescriptor.Owner.Name}}/arch/$repo/$arch"></origin-url>
|
||||
Server = {{ctx.AppFullLink}}/api/packages/{{$.PackageDescriptor.Owner.Name}}/arch/$repo/$arch
|
||||
{{end}}</code></pre></div>
|
||||
</div>
|
||||
<div class="field">
|
||||
|
||||
@@ -8,8 +8,8 @@
|
||||
default = "gitea"
|
||||
|
||||
[registries.gitea]
|
||||
index = "sparse+<origin-url data-url="{{AppSubUrl}}/api/packages/{{.PackageDescriptor.Owner.Name}}/cargo/"></origin-url>" # Sparse index
|
||||
# index = "<origin-url data-url="{{AppSubUrl}}/{{.PackageDescriptor.Owner.Name}}/_cargo-index.git"></origin-url>" # Git
|
||||
index = "sparse+{{ctx.AppFullLink}}/api/packages/{{.PackageDescriptor.Owner.Name}}/cargo/" # Sparse index
|
||||
# index = "{{ctx.AppFullLink}}/{{.PackageDescriptor.Owner.Name}}/_cargo-index.git" # Git
|
||||
|
||||
[net]
|
||||
git-fetch-with-cli = true</code></pre></div>
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<div class="ui form">
|
||||
<div class="field">
|
||||
<label>{{svg "octicon-code"}} {{ctx.Locale.Tr "packages.chef.registry"}}</label>
|
||||
<div class="markup"><pre class="code-block"><code>knife[:supermarket_site] = '<origin-url data-url="{{AppSubUrl}}/api/packages/{{.PackageDescriptor.Owner.Name}}/chef"></origin-url>'</code></pre></div>
|
||||
<div class="markup"><pre class="code-block"><code>knife[:supermarket_site] = '{{ctx.AppFullLink}}/api/packages/{{.PackageDescriptor.Owner.Name}}/chef'</code></pre></div>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>{{svg "octicon-terminal"}} {{ctx.Locale.Tr "packages.chef.install"}}</label>
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
<div class="markup"><pre class="code-block"><code>{
|
||||
"repositories": [{
|
||||
"type": "composer",
|
||||
"url": "<origin-url data-url="{{AppSubUrl}}/api/packages/{{.PackageDescriptor.Owner.Name}}/composer"></origin-url>"
|
||||
"url": "{{ctx.AppFullLink}}/api/packages/{{.PackageDescriptor.Owner.Name}}/composer"
|
||||
}
|
||||
]
|
||||
}</code></pre></div>
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<div class="ui form">
|
||||
<div class="field">
|
||||
<label>{{svg "octicon-terminal"}} {{ctx.Locale.Tr "packages.conan.registry"}}</label>
|
||||
<div class="markup"><pre class="code-block"><code>conan remote add gitea <origin-url data-url="{{AppSubUrl}}/api/packages/{{.PackageDescriptor.Owner.Name}}/conan"></origin-url></code></pre></div>
|
||||
<div class="markup"><pre class="code-block"><code>conan remote add gitea {{ctx.AppFullLink}}/api/packages/{{.PackageDescriptor.Owner.Name}}/conan</code></pre></div>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>{{svg "octicon-terminal"}} {{ctx.Locale.Tr "packages.conan.install"}}</label>
|
||||
|
||||
@@ -4,11 +4,11 @@
|
||||
<div class="ui form">
|
||||
<div class="field">
|
||||
<label>{{svg "octicon-code"}} {{ctx.Locale.Tr "packages.conda.registry"}}</label>
|
||||
<div class="markup"><pre class="code-block"><code>channel_alias: <origin-url data-url="{{AppSubUrl}}/api/packages/{{.PackageDescriptor.Owner.Name}}/conda"></origin-url>
|
||||
<div class="markup"><pre class="code-block"><code>channel_alias: {{ctx.AppFullLink}}/api/packages/{{.PackageDescriptor.Owner.Name}}/conda
|
||||
channels:
|
||||
  - <origin-url data-url="{{AppSubUrl}}/api/packages/{{.PackageDescriptor.Owner.Name}}/conda"></origin-url>
|
||||
  - {{ctx.AppFullLink}}/api/packages/{{.PackageDescriptor.Owner.Name}}/conda
|
||||
default_channels:
|
||||
  - <origin-url data-url="{{AppSubUrl}}/api/packages/{{.PackageDescriptor.Owner.Name}}/conda"></origin-url></code></pre></div>
|
||||
  - {{ctx.AppFullLink}}/api/packages/{{.PackageDescriptor.Owner.Name}}/conda</code></pre></div>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>{{svg "octicon-terminal"}} {{ctx.Locale.Tr "packages.conda.install"}}</label>
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<div class="ui form">
|
||||
<div class="field">
|
||||
<label>{{svg "octicon-code"}} {{ctx.Locale.Tr "packages.cran.registry"}}</label>
|
||||
<div class="markup"><pre class="code-block"><code>options("repos" = c(getOption("repos"), c(gitea="<origin-url data-url="{{AppSubUrl}}/api/packages/{{.PackageDescriptor.Owner.Name}}/cran"></origin-url>")))</code></pre></div>
|
||||
<div class="markup"><pre class="code-block"><code>options("repos" = c(getOption("repos"), c(gitea="{{ctx.AppFullLink}}/api/packages/{{.PackageDescriptor.Owner.Name}}/cran")))</code></pre></div>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>{{svg "octicon-terminal"}} {{ctx.Locale.Tr "packages.cran.install"}}</label>
|
||||
|
||||
@@ -4,8 +4,8 @@
|
||||
<div class="ui form">
|
||||
<div class="field">
|
||||
<label>{{svg "octicon-terminal"}} {{ctx.Locale.Tr "packages.debian.registry"}}</label>
|
||||
<div class="markup"><pre class="code-block"><code>sudo curl <origin-url data-url="{{AppSubUrl}}/api/packages/{{$.PackageDescriptor.Owner.Name}}/debian/repository.key"></origin-url> -o /etc/apt/keyrings/gitea-{{$.PackageDescriptor.Owner.Name}}.asc
|
||||
echo "deb [signed-by=/etc/apt/keyrings/gitea-{{$.PackageDescriptor.Owner.Name}}.asc] <origin-url data-url="{{AppSubUrl}}/api/packages/{{$.PackageDescriptor.Owner.Name}}/debian"></origin-url> $distribution $component" | sudo tee -a /etc/apt/sources.list.d/gitea.list
|
||||
<div class="markup"><pre class="code-block"><code>sudo curl {{ctx.AppFullLink}}/api/packages/{{$.PackageDescriptor.Owner.Name}}/debian/repository.key -o /etc/apt/keyrings/gitea-{{$.PackageDescriptor.Owner.Name}}.asc
|
||||
echo "deb [signed-by=/etc/apt/keyrings/gitea-{{$.PackageDescriptor.Owner.Name}}.asc] {{ctx.AppFullLink}}/api/packages/{{$.PackageDescriptor.Owner.Name}}/debian $distribution $component" | sudo tee -a /etc/apt/sources.list.d/gitea.list
|
||||
sudo apt update</code></pre></div>
|
||||
<p>{{ctx.Locale.Tr "packages.debian.registry.info"}}</p>
|
||||
</div>
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
<label>{{svg "octicon-terminal"}} {{ctx.Locale.Tr "packages.generic.download"}}</label>
|
||||
<div class="markup"><pre class="code-block"><code>
|
||||
{{- range .PackageDescriptor.Files -}}
|
||||
curl -OJ <origin-url data-url="{{AppSubUrl}}/api/packages/{{$.PackageDescriptor.Owner.Name}}/generic/{{$.PackageDescriptor.Package.Name}}/{{$.PackageDescriptor.Version.Version}}/{{.File.Name}}"></origin-url>
|
||||
curl -OJ {{ctx.AppFullLink}}/api/packages/{{$.PackageDescriptor.Owner.Name}}/generic/{{$.PackageDescriptor.Package.Name}}/{{$.PackageDescriptor.Version.Version}}/{{.File.Name}}
|
||||
{{end -}}
|
||||
</code></pre></div>
|
||||
</div>
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<div class="ui form">
|
||||
<div class="field">
|
||||
<label>{{svg "octicon-terminal"}} {{ctx.Locale.Tr "packages.go.install"}}</label>
|
||||
<div class="markup"><pre class="code-block"><code>GOPROXY=<origin-url data-url="{{AppSubUrl}}/api/packages/{{$.PackageDescriptor.Owner.Name}}/go"></origin-url> go install {{$.PackageDescriptor.Package.Name}}@{{$.PackageDescriptor.Version.Version}}</code></pre></div>
|
||||
<div class="markup"><pre class="code-block"><code>GOPROXY={{ctx.AppFullLink}}/api/packages/{{$.PackageDescriptor.Owner.Name}}/go go install {{$.PackageDescriptor.Package.Name}}@{{$.PackageDescriptor.Version.Version}}</code></pre></div>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>{{ctx.Locale.Tr "packages.registry.documentation" "Go" "https://docs.gitea.com/usage/packages/go"}}</label>
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<div class="ui form">
|
||||
<div class="field">
|
||||
<label>{{svg "octicon-terminal"}} {{ctx.Locale.Tr "packages.helm.registry"}}</label>
|
||||
<div class="markup"><pre class="code-block"><code>helm repo add {{AppDomain}} <origin-url data-url="{{AppSubUrl}}/api/packages/{{.PackageDescriptor.Owner.Name}}/helm"></origin-url>
|
||||
<div class="markup"><pre class="code-block"><code>helm repo add {{AppDomain}} {{ctx.AppFullLink}}/api/packages/{{.PackageDescriptor.Owner.Name}}/helm
|
||||
helm repo update</code></pre></div>
|
||||
</div>
|
||||
<div class="field">
|
||||
|
||||
@@ -11,19 +11,19 @@
|
||||
<div class="markup"><pre class="code-block"><code><repositories>
|
||||
<repository>
|
||||
<id>gitea</id>
|
||||
<url><origin-url data-url="{{AppSubUrl}}/api/packages/{{.PackageDescriptor.Owner.Name}}/maven"></origin-url></url>
|
||||
<url>{{ctx.AppFullLink}}/api/packages/{{.PackageDescriptor.Owner.Name}}/maven</url>
|
||||
</repository>
|
||||
</repositories>
|
||||
|
||||
<distributionManagement>
|
||||
<repository>
|
||||
<id>gitea</id>
|
||||
<url><origin-url data-url="{{AppSubUrl}}/api/packages/{{.PackageDescriptor.Owner.Name}}/maven"></origin-url></url>
|
||||
<url>{{ctx.AppFullLink}}/api/packages/{{.PackageDescriptor.Owner.Name}}/maven</url>
|
||||
</repository>
|
||||
|
||||
<snapshotRepository>
|
||||
<id>gitea</id>
|
||||
<url><origin-url data-url="{{AppSubUrl}}/api/packages/{{.PackageDescriptor.Owner.Name}}/maven"></origin-url></url>
|
||||
<url>{{ctx.AppFullLink}}/api/packages/{{.PackageDescriptor.Owner.Name}}/maven</url>
|
||||
</snapshotRepository>
|
||||
</distributionManagement></code></pre></div>
|
||||
</div>
|
||||
@@ -41,7 +41,7 @@
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>{{svg "octicon-terminal"}} {{ctx.Locale.Tr "packages.maven.download"}}</label>
|
||||
<div class="markup"><pre class="code-block"><code>mvn dependency:get -DremoteRepositories=<origin-url data-url="{{AppSubUrl}}/api/packages/{{.PackageDescriptor.Owner.Name}}/maven"></origin-url> -Dartifact={{.PackageDescriptor.Metadata.GroupID}}:{{.PackageDescriptor.Metadata.ArtifactID}}:{{.PackageDescriptor.Version.Version}}</code></pre></div>
|
||||
<div class="markup"><pre class="code-block"><code>mvn dependency:get -DremoteRepositories={{ctx.AppFullLink}}/api/packages/{{.PackageDescriptor.Owner.Name}}/maven -Dartifact={{.PackageDescriptor.Metadata.GroupID}}:{{.PackageDescriptor.Metadata.ArtifactID}}:{{.PackageDescriptor.Version.Version}}</code></pre></div>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>{{ctx.Locale.Tr "packages.registry.documentation" "Maven" "https://docs.gitea.com/usage/packages/maven/"}}</label>
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<div class="ui form">
|
||||
<div class="field">
|
||||
<label>{{svg "octicon-code"}} {{ctx.Locale.Tr "packages.npm.registry"}}</label>
|
||||
<div class="markup"><pre class="code-block"><code>{{if .PackageDescriptor.Metadata.Scope}}{{.PackageDescriptor.Metadata.Scope}}:{{end}}registry=<origin-url data-url="{{AppSubUrl}}/api/packages/{{.PackageDescriptor.Owner.Name}}/npm/"></origin-url></code></pre></div>
|
||||
<div class="markup"><pre class="code-block"><code>{{if .PackageDescriptor.Metadata.Scope}}{{.PackageDescriptor.Metadata.Scope}}:{{end}}registry={{ctx.AppFullLink}}/api/packages/{{.PackageDescriptor.Owner.Name}}/npm/</code></pre></div>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>{{svg "octicon-terminal"}} {{ctx.Locale.Tr "packages.npm.install"}}</label>
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<div class="ui form">
|
||||
<div class="field">
|
||||
<label>{{svg "octicon-terminal"}} {{ctx.Locale.Tr "packages.nuget.registry"}}</label>
|
||||
<div class="markup"><pre class="code-block"><code>dotnet nuget add source --name {{.PackageDescriptor.Owner.Name}} --username your_username --password your_token <origin-url data-url="{{AppSubUrl}}/api/packages/{{.PackageDescriptor.Owner.Name}}/nuget/index.json"></origin-url></code></pre></div>
|
||||
<div class="markup"><pre class="code-block"><code>dotnet nuget add source --name {{.PackageDescriptor.Owner.Name}} --username your_username --password your_token {{ctx.AppFullLink}}/api/packages/{{.PackageDescriptor.Owner.Name}}/nuget/index.json</code></pre></div>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>{{svg "octicon-terminal"}} {{ctx.Locale.Tr "packages.nuget.install"}}</label>
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<div class="ui form">
|
||||
<div class="field">
|
||||
<label>{{svg "octicon-terminal"}} {{ctx.Locale.Tr "packages.pub.install"}}</label>
|
||||
<div class="markup"><pre class="code-block"><code>dart pub add {{.PackageDescriptor.Package.Name}}:{{.PackageDescriptor.Version.Version}} --hosted-url=<origin-url data-url="{{AppSubUrl}}/api/packages/{{.PackageDescriptor.Owner.Name}}/pub/"></origin-url></code></pre></div>
|
||||
<div class="markup"><pre class="code-block"><code>dart pub add {{.PackageDescriptor.Package.Name}}:{{.PackageDescriptor.Version.Version}} --hosted-url={{ctx.AppFullLink}}/api/packages/{{.PackageDescriptor.Owner.Name}}/pub/</code></pre></div>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>{{ctx.Locale.Tr "packages.registry.documentation" "Pub" "https://docs.gitea.com/usage/packages/pub/"}}</label>
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<div class="ui form">
|
||||
<div class="field">
|
||||
<label>{{svg "octicon-terminal"}} {{ctx.Locale.Tr "packages.pypi.install"}}</label>
|
||||
<div class="markup"><pre class="code-block"><code>pip install --index-url <origin-url data-url="{{AppSubUrl}}/api/packages/{{.PackageDescriptor.Owner.Name}}/pypi/simple/"></origin-url> --extra-index-url https://pypi.org/simple {{.PackageDescriptor.Package.Name}}</code></pre></div>
|
||||
<div class="markup"><pre class="code-block"><code>pip install --index-url {{ctx.AppFullLink}}/api/packages/{{.PackageDescriptor.Owner.Name}}/pypi/simple/ --extra-index-url https://pypi.org/simple {{.PackageDescriptor.Package.Name}}</code></pre></div>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>{{ctx.Locale.Tr "packages.registry.documentation" "PyPI" "https://docs.gitea.com/usage/packages/pypi/"}}</label>
|
||||
|
||||
@@ -11,19 +11,19 @@
|
||||
# {{ctx.Locale.Tr "packages.rpm.distros.redhat"}}
|
||||
{{- range $group := .Groups}}
|
||||
{{- if $group}}{{$group = print "/" $group}}{{end}}
|
||||
dnf config-manager --add-repo <origin-url data-url="{{AppSubUrl}}/api/packages/{{$.PackageDescriptor.Owner.Name}}/rpm{{$group}}.repo"></origin-url>
|
||||
dnf config-manager --add-repo {{ctx.AppFullLink}}/api/packages/{{$.PackageDescriptor.Owner.Name}}/rpm{{$group}}.repo
|
||||
{{- end}}
|
||||
|
||||
# Fedora 41+ (DNF5)
|
||||
{{- range $group := .Groups}}
|
||||
{{- if $group}}{{$group = print "/" $group}}{{end}}
|
||||
dnf config-manager addrepo --from-repofile=<origin-url data-url="{{AppSubUrl}}/api/packages/{{$.PackageDescriptor.Owner.Name}}/rpm{{$group}}.repo"></origin-url>
|
||||
dnf config-manager addrepo --from-repofile={{ctx.AppFullLink}}/api/packages/{{$.PackageDescriptor.Owner.Name}}/rpm{{$group}}.repo
|
||||
{{- end}}
|
||||
|
||||
# {{ctx.Locale.Tr "packages.rpm.distros.suse"}}
|
||||
{{- range $group := .Groups}}
|
||||
{{- if $group}}{{$group = print "/" $group}}{{end}}
|
||||
zypper addrepo <origin-url data-url="{{AppSubUrl}}/api/packages/{{$.PackageDescriptor.Owner.Name}}/rpm{{$group}}.repo"></origin-url>
|
||||
zypper addrepo {{ctx.AppFullLink}}/api/packages/{{$.PackageDescriptor.Owner.Name}}/rpm{{$group}}.repo
|
||||
{{- end}}</code></pre></div>
|
||||
</div>
|
||||
<div class="field">
|
||||
|
||||
@@ -4,11 +4,11 @@
|
||||
<div class="ui form">
|
||||
<div class="field">
|
||||
<label>{{svg "octicon-terminal"}} {{ctx.Locale.Tr "packages.rubygems.install"}}:</label>
|
||||
<div class="markup"><pre class="code-block"><code>gem install {{.PackageDescriptor.Package.Name}} --version "{{.PackageDescriptor.Version.Version}}" --source "<origin-url data-url="{{AppSubUrl}}/api/packages/{{.PackageDescriptor.Owner.Name}}/rubygems"></origin-url>"</code></pre></div>
|
||||
<div class="markup"><pre class="code-block"><code>gem install {{.PackageDescriptor.Package.Name}} --version "{{.PackageDescriptor.Version.Version}}" --source "{{ctx.AppFullLink}}/api/packages/{{.PackageDescriptor.Owner.Name}}/rubygems"</code></pre></div>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>{{svg "octicon-code"}} {{ctx.Locale.Tr "packages.rubygems.install2"}}:</label>
|
||||
<div class="markup"><pre class="code-block"><code>source "<origin-url data-url="{{AppSubUrl}}/api/packages/{{.PackageDescriptor.Owner.Name}}/rubygems"></origin-url>" do
|
||||
<div class="markup"><pre class="code-block"><code>source "{{ctx.AppFullLink}}/api/packages/{{.PackageDescriptor.Owner.Name}}/rubygems" do
|
||||
gem "{{.PackageDescriptor.Package.Name}}", "{{.PackageDescriptor.Version.Version}}"
|
||||
end</code></pre></div>
|
||||
</div>
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<div class="ui form">
|
||||
<div class="field">
|
||||
<label>{{svg "octicon-terminal"}} {{ctx.Locale.Tr "packages.swift.registry"}}</label>
|
||||
<div class="markup"><pre class="code-block"><code>swift package-registry set <origin-url data-url="{{AppSubUrl}}/api/packages/{{.PackageDescriptor.Owner.Name}}/swift"></origin-url></code></pre></div>
|
||||
<div class="markup"><pre class="code-block"><code>swift package-registry set {{ctx.AppFullLink}}/api/packages/{{.PackageDescriptor.Owner.Name}}/swift</code></pre></div>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>{{svg "octicon-code"}} {{ctx.Locale.Tr "packages.swift.install"}}</label>
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<div class="ui form">
|
||||
<div class="field">
|
||||
<label>{{svg "octicon-terminal"}} {{ctx.Locale.Tr "packages.vagrant.install"}}</label>
|
||||
<div class="markup"><pre class="code-block"><code>vagrant box add --box-version {{.PackageDescriptor.Version.Version}} "<origin-url data-url="{{AppSubUrl}}/api/packages/{{.PackageDescriptor.Owner.Name}}/vagrant/{{.PackageDescriptor.Package.Name}}"></origin-url>"</code></pre></div>
|
||||
<div class="markup"><pre class="code-block"><code>vagrant box add --box-version {{.PackageDescriptor.Version.Version}} "{{ctx.AppFullLink}}/api/packages/{{.PackageDescriptor.Owner.Name}}/vagrant/{{.PackageDescriptor.Package.Name}}"</code></pre></div>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>{{ctx.Locale.Tr "packages.registry.documentation" "Vagrant" "https://docs.gitea.com/usage/packages/vagrant/"}}</label>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{{$canWriteProject := and .CanWriteProjects (or (not .Repository) (not .Repository.IsArchived))}}
|
||||
|
||||
<div class="ui container fluid padded projects-view">
|
||||
<div class="ui container fluid padded projects-view" data-global-init="initRepoProjectsView">
|
||||
<div class="ui container flex-text-block project-header">
|
||||
<h2>{{.Project.Title}}</h2>
|
||||
<div class="tw-flex-1"></div>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{{if ctx.RootData.IsSigned}}
|
||||
<div class="item action ui dropdown jump pointing top right select-reaction" data-action-url="{{.ActionURL}}">
|
||||
<div class="item action ui dropdown jump pointing top right select-reaction" data-action-url="{{.ActionURL}}" aria-label="{{ctx.Locale.Tr "repo.reactions"}}">
|
||||
<a class="muted">{{svg "octicon-smiley"}}</a>
|
||||
<div class="menu">
|
||||
{{range $value := AllowedReactions}}
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
<div class="ui secondary segment tw-font-mono">
|
||||
{{$gitRemoteName := ctx.RootData.SystemConfig.Repository.GitGuideRemoteName.Value ctx}}
|
||||
{{if eq .PullRequest.Flow 0}}
|
||||
<div>git fetch -u {{if ne .PullRequest.HeadRepo.ID .PullRequest.BaseRepo.ID}}<origin-url data-url="{{.PullRequest.HeadRepo.Link}}"></origin-url>{{else}}{{$gitRemoteName}}{{end}} {{.PullRequest.HeadBranch}}:{{$localBranch}}</div>
|
||||
<div>git fetch -u {{if ne .PullRequest.HeadRepo.ID .PullRequest.BaseRepo.ID}}{{ctx.AppFullLink .PullRequest.HeadRepo.Link}}{{else}}{{$gitRemoteName}}{{end}} {{.PullRequest.HeadBranch}}:{{$localBranch}}</div>
|
||||
{{else}}
|
||||
<div>git fetch -u {{$gitRemoteName}} {{.PullRequest.GetGitHeadRefName}}:{{$localBranch}}</div>
|
||||
{{end}}
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
<div class="bottom-reactions" data-action-url="{{$.ActionURL}}">
|
||||
<div class="bottom-reactions" data-action-url="{{$.ActionURL}}" role="group" aria-label="{{ctx.Locale.Tr "repo.reactions"}}">
|
||||
{{range $key, $value := .Reactions}}
|
||||
{{$hasReacted := $value.HasUser ctx.RootData.SignedUserID}}
|
||||
<a role="button" class="ui label basic{{if $hasReacted}} primary{{end}}{{if not ctx.RootData.IsSigned}} disabled{{end}}"
|
||||
data-global-click="onCommentReactionButtonClick"
|
||||
data-tooltip-content title="{{$value.GetFirstUsers}}{{if gt ($value.GetMoreUserCount) 0}} {{ctx.Locale.Tr "repo.reactions_more" $value.GetMoreUserCount}}{{end}}"
|
||||
aria-label="{{$value.GetFirstUsers}}{{if gt ($value.GetMoreUserCount) 0}} {{ctx.Locale.Tr "repo.reactions_more" $value.GetMoreUserCount}}{{end}}"
|
||||
aria-label="{{$key}}: {{$value.GetFirstUsers}}{{if gt ($value.GetMoreUserCount) 0}} {{ctx.Locale.Tr "repo.reactions_more" $value.GetMoreUserCount}}{{end}}"
|
||||
data-tooltip-placement="bottom-start"
|
||||
data-reaction-content="{{$key}}" data-has-reacted="{{$hasReacted}}">
|
||||
<span class="reaction">{{ReactionToEmoji $key}}</span>
|
||||
|
||||
@@ -9,22 +9,21 @@
|
||||
<div class="ui small fluid action input">
|
||||
{{template "shared/search/input" dict "Value" .Value "Disabled" .Disabled "Placeholder" .Placeholder}}
|
||||
{{if .SearchModes}}
|
||||
<div class="ui small dropdown selection {{if .Disabled}}disabled{{end}}" data-tooltip-content="{{ctx.Locale.Tr "search.type_tooltip"}}">
|
||||
<div class="text"></div> {{svg "octicon-triangle-down" 14 "dropdown icon"}}
|
||||
<input name="search_mode" type="hidden" value="
|
||||
{{- if .SelectedSearchMode -}}
|
||||
{{- .SelectedSearchMode -}}
|
||||
{{- else -}}
|
||||
{{- $defaultSearchMode := index .SearchModes 0 -}}
|
||||
{{- $defaultSearchMode.ModeValue -}}
|
||||
{{- end -}}
|
||||
">
|
||||
<div class="menu">
|
||||
{{range $mode := .SearchModes}}
|
||||
<div class="item" data-value="{{$mode.ModeValue}}" data-tooltip-content="{{ctx.Locale.Tr $mode.TooltipTrKey}}">{{ctx.Locale.Tr $mode.TitleTrKey}}</div>
|
||||
{{$selected := index .SearchModes 0}}
|
||||
{{range $mode := .SearchModes}}
|
||||
{{if eq $mode.ModeValue $.SelectedSearchMode}}
|
||||
{{$selected = $mode}}
|
||||
{{end}}
|
||||
{{end}}
|
||||
<div class="ui small dropdown selection {{if .Disabled}}disabled{{end}}" data-tooltip-content="{{ctx.Locale.Tr "search.type_tooltip"}}">
|
||||
<div class="text">{{ctx.Locale.Tr $selected.TitleTrKey}}</div> {{svg "octicon-triangle-down" 14 "dropdown icon"}}
|
||||
<input name="search_mode" type="hidden" value="{{$selected.ModeValue}}">
|
||||
<div class="menu">
|
||||
{{range $mode := .SearchModes}}
|
||||
<div class="item" data-value="{{$mode.ModeValue}}" data-tooltip-content="{{ctx.Locale.Tr $mode.TooltipTrKey}}">{{ctx.Locale.Tr $mode.TitleTrKey}}</div>
|
||||
{{end}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
{{template "shared/search/button" dict "Disabled" .Disabled "Tooltip" .Tooltip}}
|
||||
</div>
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
import {env} from 'node:process';
|
||||
import {expect, test} from '@playwright/test';
|
||||
import {login, apiCreateRepo, apiDeleteRepo} from './utils.ts';
|
||||
import {login, apiCreateRepo, apiDeleteRepo, randomString} from './utils.ts';
|
||||
|
||||
test('codeeditor textarea updates correctly', async ({page, request}) => {
|
||||
const repoName = `e2e-codeeditor-${Date.now()}`;
|
||||
await apiCreateRepo(request, {name: repoName});
|
||||
const repoName = `e2e-codeeditor-${randomString(8)}`;
|
||||
await Promise.all([apiCreateRepo(request, {name: repoName}), login(page)]);
|
||||
try {
|
||||
await login(page);
|
||||
await page.goto(`/${env.GITEA_TEST_E2E_USER}/${repoName}/_new/main`);
|
||||
await page.getByPlaceholder('Name your file…').fill('test.js');
|
||||
await expect(page.locator('.editor-loading')).toBeHidden();
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
import {test, expect} from '@playwright/test';
|
||||
import {loginUser, baseUrl, apiUserHeaders, apiCreateUser, apiDeleteUser, apiCreateRepo, apiCreateIssue, apiStartStopwatch, timeoutFactor} from './utils.ts';
|
||||
import {loginUser, baseUrl, apiUserHeaders, apiCreateUser, apiDeleteUser, apiCreateRepo, apiCreateIssue, apiStartStopwatch, timeoutFactor, randomString} from './utils.ts';
|
||||
|
||||
// These tests rely on a short EVENT_SOURCE_UPDATE_TIME in the e2e server config.
|
||||
test.describe('events', () => {
|
||||
test('notification count', async ({page, request}) => {
|
||||
const id = `ev-notif-${Date.now()}-${Math.random().toString(36).slice(2, 6)}`;
|
||||
const owner = `${id}-owner`;
|
||||
const commenter = `${id}-commenter`;
|
||||
const repoName = id;
|
||||
const owner = `ev-notif-owner-${randomString(8)}`;
|
||||
const commenter = `ev-notif-commenter-${randomString(8)}`;
|
||||
const repoName = `ev-notif-${randomString(8)}`;
|
||||
|
||||
await Promise.all([apiCreateUser(request, owner), apiCreateUser(request, commenter)]);
|
||||
|
||||
@@ -30,7 +29,7 @@ test.describe('events', () => {
|
||||
});
|
||||
|
||||
test('stopwatch', async ({page, request}) => {
|
||||
const name = `ev-sw-${Date.now()}-${Math.random().toString(36).slice(2, 6)}`;
|
||||
const name = `ev-sw-${randomString(8)}`;
|
||||
const headers = apiUserHeaders(name);
|
||||
|
||||
await apiCreateUser(request, name);
|
||||
@@ -52,7 +51,7 @@ test.describe('events', () => {
|
||||
});
|
||||
|
||||
test('logout propagation', async ({browser, request}) => {
|
||||
const name = `ev-logout-${Date.now()}-${Math.random().toString(36).slice(2, 6)}`;
|
||||
const name = `ev-logout-${randomString(8)}`;
|
||||
|
||||
await apiCreateUser(request, name);
|
||||
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
import {env} from 'node:process';
|
||||
import {test, expect} from '@playwright/test';
|
||||
import {login, apiCreateRepo, apiDeleteRepo} from './utils.ts';
|
||||
import {login, apiCreateRepo, apiDeleteRepo, randomString} from './utils.ts';
|
||||
|
||||
test('create a milestone', async ({page}) => {
|
||||
const repoName = `e2e-milestone-${Date.now()}`;
|
||||
await login(page);
|
||||
await apiCreateRepo(page.request, {name: repoName});
|
||||
const repoName = `e2e-milestone-${randomString(8)}`;
|
||||
await Promise.all([login(page), apiCreateRepo(page.request, {name: repoName})]);
|
||||
await page.goto(`/${env.GITEA_TEST_E2E_USER}/${repoName}/milestones/new`);
|
||||
await page.getByPlaceholder('Title').fill('Test Milestone');
|
||||
await page.getByRole('button', {name: 'Create Milestone'}).click();
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import {test, expect} from '@playwright/test';
|
||||
import {login, apiDeleteOrg} from './utils.ts';
|
||||
import {login, apiDeleteOrg, randomString} from './utils.ts';
|
||||
|
||||
test('create an organization', async ({page}) => {
|
||||
const orgName = `e2e-org-${Date.now()}`;
|
||||
const orgName = `e2e-org-${randomString(8)}`;
|
||||
await login(page);
|
||||
await page.goto('/org/create');
|
||||
await page.getByLabel('Organization Name').fill(orgName);
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
import {env} from 'node:process';
|
||||
import {expect, test} from '@playwright/test';
|
||||
import {login, apiCreateRepo, apiCreateIssue, apiDeleteRepo, randomString} from './utils.ts';
|
||||
|
||||
test('toggle issue reactions', async ({page, request}) => {
|
||||
const repoName = `e2e-reactions-${randomString(8)}`;
|
||||
const owner = env.GITEA_TEST_E2E_USER;
|
||||
await apiCreateRepo(request, {name: repoName});
|
||||
await Promise.all([
|
||||
apiCreateIssue(request, owner, repoName, {title: 'Reaction test'}),
|
||||
login(page),
|
||||
]);
|
||||
try {
|
||||
await page.goto(`/${owner}/${repoName}/issues/1`);
|
||||
|
||||
const issueComment = page.locator('.timeline-item.comment.first');
|
||||
|
||||
const reactionPicker = issueComment.locator('.select-reaction');
|
||||
await reactionPicker.click();
|
||||
await reactionPicker.getByLabel('+1').click();
|
||||
|
||||
const reactions = issueComment.getByRole('group', {name: 'Reactions'});
|
||||
await expect(reactions.getByRole('button', {name: /^\+1:/})).toContainText('1');
|
||||
|
||||
await reactions.getByRole('button', {name: /^\+1:/}).click();
|
||||
await expect(reactions.getByRole('button', {name: /^\+1:/})).toHaveCount(0);
|
||||
} finally {
|
||||
await apiDeleteRepo(request, owner, repoName);
|
||||
}
|
||||
});
|
||||
@@ -1,9 +1,9 @@
|
||||
import {env} from 'node:process';
|
||||
import {test, expect} from '@playwright/test';
|
||||
import {apiCreateRepo, apiDeleteRepo} from './utils.ts';
|
||||
import {apiCreateRepo, apiDeleteRepo, randomString} from './utils.ts';
|
||||
|
||||
test('repo readme', async ({page}) => {
|
||||
const repoName = `e2e-readme-${Date.now()}`;
|
||||
const repoName = `e2e-readme-${randomString(8)}`;
|
||||
await apiCreateRepo(page.request, {name: repoName});
|
||||
await page.goto(`/${env.GITEA_TEST_E2E_USER}/${repoName}`);
|
||||
await expect(page.locator('#readme')).toContainText(repoName);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import {env} from 'node:process';
|
||||
import {test, expect} from '@playwright/test';
|
||||
import {login, logout, apiDeleteUser} from './utils.ts';
|
||||
import {login, logout, apiDeleteUser, randomString} from './utils.ts';
|
||||
|
||||
test.beforeEach(async ({page}) => {
|
||||
await page.goto('/user/sign_up');
|
||||
@@ -32,7 +32,7 @@ test('register with mismatched passwords shows error', async ({page}) => {
|
||||
});
|
||||
|
||||
test('register then login', async ({page}) => {
|
||||
const username = `e2e-register-${Date.now()}`;
|
||||
const username = `e2e-register-${randomString(8)}`;
|
||||
const email = `${username}@${env.GITEA_TEST_E2E_DOMAIN}`;
|
||||
const password = 'password123!';
|
||||
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import {env} from 'node:process';
|
||||
import {test} from '@playwright/test';
|
||||
import {login, apiDeleteRepo} from './utils.ts';
|
||||
import {login, apiDeleteRepo, randomString} from './utils.ts';
|
||||
|
||||
test('create a repository', async ({page}) => {
|
||||
const repoName = `e2e-repo-${Date.now()}`;
|
||||
const repoName = `e2e-repo-${randomString(8)}`;
|
||||
await login(page);
|
||||
await page.goto('/repo/create');
|
||||
await page.locator('input[name="repo_name"]').fill(repoName);
|
||||
|
||||
@@ -1,14 +1,20 @@
|
||||
import {test, expect} from '@playwright/test';
|
||||
import {login} from './utils.ts';
|
||||
import {loginUser, apiCreateUser, apiDeleteUser, randomString} from './utils.ts';
|
||||
|
||||
test('update profile biography', async ({page}) => {
|
||||
const bio = `e2e-bio-${Date.now()}`;
|
||||
await login(page);
|
||||
await page.goto('/user/settings');
|
||||
await page.getByLabel('Biography').fill(bio);
|
||||
await page.getByRole('button', {name: 'Update Profile'}).click();
|
||||
await expect(page.getByLabel('Biography')).toHaveValue(bio);
|
||||
await page.getByLabel('Biography').fill('');
|
||||
await page.getByRole('button', {name: 'Update Profile'}).click();
|
||||
await expect(page.getByLabel('Biography')).toHaveValue('');
|
||||
test('update profile biography', async ({page, request}) => {
|
||||
const username = `e2e-settings-${randomString(8)}`;
|
||||
const bio = `e2e-bio-${randomString(8)}`;
|
||||
await apiCreateUser(request, username);
|
||||
try {
|
||||
await loginUser(page, username);
|
||||
await page.goto('/user/settings');
|
||||
await page.getByLabel('Biography').fill(bio);
|
||||
await page.getByRole('button', {name: 'Update Profile'}).click();
|
||||
await expect(page.getByLabel('Biography')).toHaveValue(bio);
|
||||
await page.getByLabel('Biography').fill('');
|
||||
await page.getByRole('button', {name: 'Update Profile'}).click();
|
||||
await expect(page.getByLabel('Biography')).toHaveValue('');
|
||||
} finally {
|
||||
await apiDeleteUser(request, username);
|
||||
}
|
||||
});
|
||||
|
||||
+13
-15
@@ -1,7 +1,16 @@
|
||||
import {randomBytes} from 'node:crypto';
|
||||
import {env} from 'node:process';
|
||||
import {expect} from '@playwright/test';
|
||||
import type {APIRequestContext, Locator, Page} from '@playwright/test';
|
||||
import type {APIRequestContext, Page} from '@playwright/test';
|
||||
|
||||
/** Generate a random alphanumeric string. */
|
||||
export function randomString(length: number): string {
|
||||
const chars = 'abcdefghijklmnopqrstuvwxyz0123456789';
|
||||
let result = '';
|
||||
for (let index = 0; index < length; index++) {
|
||||
result += chars.charAt(Math.floor(Math.random() * chars.length));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
export const timeoutFactor = Number(env.GITEA_TEST_E2E_TIMEOUT_FACTOR) || 1;
|
||||
|
||||
@@ -63,14 +72,8 @@ export async function apiDeleteOrg(requestContext: APIRequestContext, name: stri
|
||||
}), 'apiDeleteOrg');
|
||||
}
|
||||
|
||||
/** Generate a random password that satisfies the complexity requirements. */
|
||||
function generatePassword() {
|
||||
const chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
|
||||
return `${Array.from(randomBytes(12), (b) => chars[b % chars.length]).join('')}!aA1`;
|
||||
}
|
||||
|
||||
/** Random password shared by all test users — used for both API user creation and browser login. */
|
||||
const testUserPassword = generatePassword();
|
||||
/** Password shared by all test users — used for both API user creation and browser login. */
|
||||
const testUserPassword = 'e2e-password!aA1';
|
||||
|
||||
export function apiUserHeaders(username: string) {
|
||||
return apiAuthHeader(username, testUserPassword);
|
||||
@@ -89,11 +92,6 @@ export async function apiDeleteUser(requestContext: APIRequestContext, username:
|
||||
}), 'apiDeleteUser');
|
||||
}
|
||||
|
||||
export async function clickDropdownItem(page: Page, trigger: Locator, itemText: string) {
|
||||
await trigger.click();
|
||||
await page.getByText(itemText).click();
|
||||
}
|
||||
|
||||
export async function loginUser(page: Page, username: string) {
|
||||
return login(page, username, testUserPassword);
|
||||
}
|
||||
|
||||
@@ -362,15 +362,20 @@ func testAPIRenameBranch(t *testing.T, doerName, ownerName, repoName, from, to s
|
||||
func TestAPIBranchProtection(t *testing.T) {
|
||||
defer tests.PrepareTestEnv(t)()
|
||||
|
||||
// Branch protection on branch that not exist
|
||||
testAPICreateBranchProtection(t, "master/doesnotexist", 1, http.StatusCreated)
|
||||
// Can create branch protection on branch that not exist
|
||||
testAPICreateBranchProtection(t, "non-existing/branch", 1, http.StatusCreated)
|
||||
testAPIGetBranchProtection(t, "non-existing/branch", http.StatusOK)
|
||||
testAPIDeleteBranchProtection(t, "non-existing/branch", http.StatusNoContent)
|
||||
|
||||
// Get branch protection on branch that exist but not branch protection
|
||||
testAPIGetBranchProtection(t, "master", http.StatusNotFound)
|
||||
|
||||
testAPICreateBranchProtection(t, "master", 2, http.StatusCreated)
|
||||
testAPICreateBranchProtection(t, "master", 1, http.StatusCreated)
|
||||
// Can only create once
|
||||
testAPICreateBranchProtection(t, "master", 0, http.StatusForbidden)
|
||||
|
||||
testAPICreateBranchProtection(t, "other-branch", 2, http.StatusCreated)
|
||||
|
||||
// Can't delete a protected branch
|
||||
testAPIDeleteBranch(t, "master", http.StatusForbidden)
|
||||
|
||||
|
||||
@@ -32,7 +32,6 @@ for (const path of globSync('web_src/css/themes/*.css', {cwd: import.meta.dirnam
|
||||
const webComponents = new Set([
|
||||
// our own, in web_src/js/webcomponents
|
||||
'overflow-menu',
|
||||
'origin-url',
|
||||
'relative-time',
|
||||
// from dependencies
|
||||
'markdown-toolbar',
|
||||
|
||||
@@ -381,7 +381,7 @@ function toggleTimeDisplay(type: 'seconds' | 'stamp') {
|
||||
|
||||
function toggleFullScreenMode() {
|
||||
isFullScreen.value = !isFullScreen.value;
|
||||
toggleFullScreen('.action-view-right', isFullScreen.value, '.action-view-body');
|
||||
toggleFullScreen(document.querySelector('.action-view-right')!, isFullScreen.value, '.action-view-body');
|
||||
}
|
||||
|
||||
async function hashChangeListener() {
|
||||
|
||||
@@ -5,6 +5,8 @@ import {fomanticQuery} from '../modules/fomantic/base.ts';
|
||||
import {queryElemChildren, queryElems, toggleElem} from '../utils/dom.ts';
|
||||
import type {SortableEvent} from 'sortablejs';
|
||||
import {toggleFullScreen} from '../utils.ts';
|
||||
import {registerGlobalInitFunc} from '../modules/observer.ts';
|
||||
import {localUserSettings} from '../modules/user-settings.ts';
|
||||
|
||||
function updateIssueCount(card: HTMLElement): void {
|
||||
const parent = card.parentElement!;
|
||||
@@ -143,27 +145,42 @@ function initRepoProjectColumnEdit(writableProjectBoard: Element): void {
|
||||
});
|
||||
}
|
||||
|
||||
function initRepoProjectToggleFullScreen(): void {
|
||||
function initRepoProjectToggleFullScreen(elProjectsView: HTMLElement): void {
|
||||
const enterFullscreenBtn = document.querySelector('.screen-full');
|
||||
const exitFullscreenBtn = document.querySelector('.screen-normal');
|
||||
if (!enterFullscreenBtn || !exitFullscreenBtn) return;
|
||||
|
||||
const settingKey = 'projects-view-options';
|
||||
type ProjectsViewOptions = {
|
||||
fullScreen: boolean;
|
||||
};
|
||||
const opts = localUserSettings.getJsonObject<ProjectsViewOptions>(settingKey, {fullScreen: false});
|
||||
const toggleFullscreenState = (isFullScreen: boolean) => {
|
||||
toggleFullScreen('.projects-view', isFullScreen);
|
||||
toggleFullScreen(elProjectsView, isFullScreen);
|
||||
toggleElem(enterFullscreenBtn, !isFullScreen);
|
||||
toggleElem(exitFullscreenBtn, isFullScreen);
|
||||
|
||||
opts.fullScreen = isFullScreen;
|
||||
localUserSettings.setJsonObject(settingKey, opts);
|
||||
};
|
||||
|
||||
enterFullscreenBtn.addEventListener('click', () => toggleFullscreenState(true));
|
||||
exitFullscreenBtn.addEventListener('click', () => toggleFullscreenState(false));
|
||||
if (opts.fullScreen) {
|
||||
// a temporary solution to remember the full screen state, not perfect,
|
||||
// just make UX better than before, especially for users who need to change the label filter frequently and want to keep full screen mode.
|
||||
toggleFullscreenState(true);
|
||||
}
|
||||
}
|
||||
|
||||
export function initRepoProject(): void {
|
||||
initRepoProjectToggleFullScreen();
|
||||
export function initRepoProjectsView(): void {
|
||||
registerGlobalInitFunc('initRepoProjectsView', (elProjectsView) => {
|
||||
initRepoProjectToggleFullScreen(elProjectsView);
|
||||
|
||||
const writableProjectBoard = document.querySelector('#project-board[data-project-board-writable="true"]');
|
||||
if (!writableProjectBoard) return;
|
||||
const writableProjectBoard = document.querySelector('#project-board[data-project-board-writable="true"]');
|
||||
if (!writableProjectBoard) return;
|
||||
|
||||
initRepoProjectSortable(); // no await
|
||||
initRepoProjectColumnEdit(writableProjectBoard);
|
||||
initRepoProjectSortable(); // no await
|
||||
initRepoProjectColumnEdit(writableProjectBoard);
|
||||
});
|
||||
}
|
||||
|
||||
+2
-2
@@ -9,7 +9,7 @@ import {initRepoGraphGit} from './features/repo-graph.ts';
|
||||
import {initHeatmap} from './features/heatmap.ts';
|
||||
import {initImageDiff} from './features/imagediff.ts';
|
||||
import {initRepoMigration} from './features/repo-migration.ts';
|
||||
import {initRepoProject} from './features/repo-projects.ts';
|
||||
import {initRepoProjectsView} from './features/repo-projects.ts';
|
||||
import {initTableSort} from './features/tablesort.ts';
|
||||
import {initAdminUserListSearchForm} from './features/admin/users.ts';
|
||||
import {initAdminConfigs} from './features/admin/config.ts';
|
||||
@@ -132,7 +132,7 @@ const initPerformanceTracer = callInitFunctions([
|
||||
initRepoIssueFilterItemLabel,
|
||||
initRepoMigration,
|
||||
initRepoMigrationStatusChecker,
|
||||
initRepoProject,
|
||||
initRepoProjectsView,
|
||||
initRepoPullRequestReview,
|
||||
initRepoReleaseNew,
|
||||
initRepoTopicBar,
|
||||
|
||||
+2
-3
@@ -208,7 +208,7 @@ export function isVideoFile({name, type}: {name?: string, type?: string}): boole
|
||||
return Boolean(/\.(mpe?g|mp4|mkv|webm)$/i.test(name || '') || type?.startsWith('video/'));
|
||||
}
|
||||
|
||||
export function toggleFullScreen(fullscreenElementsSelector: string, isFullScreen: boolean, sourceParentSelector?: string): void {
|
||||
export function toggleFullScreen(fullScreenEl: HTMLElement, isFullScreen: boolean, sourceParentSelector?: string): void {
|
||||
// hide other elements
|
||||
const headerEl = document.querySelector('#navbar')!;
|
||||
const contentEl = document.querySelector('.page-content')!;
|
||||
@@ -218,9 +218,8 @@ export function toggleFullScreen(fullscreenElementsSelector: string, isFullScree
|
||||
toggleElem(footerEl, !isFullScreen);
|
||||
|
||||
const sourceParentEl = sourceParentSelector ? document.querySelector(sourceParentSelector)! : contentEl;
|
||||
const fullScreenEl = document.querySelector(fullscreenElementsSelector)!;
|
||||
const outerEl = document.querySelector('.full.height')!;
|
||||
toggleElemClass(fullscreenElementsSelector, 'fullscreen', isFullScreen);
|
||||
toggleElemClass(fullScreenEl, 'fullscreen', isFullScreen);
|
||||
if (isFullScreen) {
|
||||
outerEl.append(fullScreenEl);
|
||||
} else {
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import './polyfills.ts';
|
||||
import './relative-time.ts';
|
||||
import './origin-url.ts';
|
||||
import './overflow-menu.ts';
|
||||
import {isDarkTheme} from '../utils.ts';
|
||||
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
import {toOriginUrl} from '../utils/url.ts';
|
||||
|
||||
window.customElements.define('origin-url', class extends HTMLElement {
|
||||
connectedCallback() {
|
||||
this.textContent = toOriginUrl(this.getAttribute('data-url')!);
|
||||
}
|
||||
});
|
||||
Reference in New Issue
Block a user