diff --git a/.drone.yml b/.drone.yml index 7810d3d10444..4e7789ef9233 100644 --- a/.drone.yml +++ b/.drone.yml @@ -33,7 +33,7 @@ steps: - git fetch --tags --force - name: deps-frontend - image: node:18 + image: node:20 pull: always commands: - make deps-frontend @@ -51,7 +51,7 @@ steps: image: techknowlogick/xgo:go-1.20.x pull: always commands: - # Upgrade to node 18 once https://github.com/techknowlogick/xgo/issues/163 is resolved + # Upgrade to node 20 once https://github.com/techknowlogick/xgo/issues/163 is resolved - curl -sL https://deb.nodesource.com/setup_16.x | bash - && apt-get -qqy install nodejs - export PATH=$PATH:$GOPATH/bin - make release @@ -161,7 +161,7 @@ steps: - git fetch --tags --force - name: deps-frontend - image: node:18 + image: node:20 pull: always commands: - make deps-frontend @@ -179,7 +179,7 @@ steps: image: techknowlogick/xgo:go-1.20.x pull: always commands: - # Upgrade to node 18 once https://github.com/techknowlogick/xgo/issues/163 is resolved + # Upgrade to node 20 once https://github.com/techknowlogick/xgo/issues/163 is resolved - curl -sL https://deb.nodesource.com/setup_16.x | bash - && apt-get -qqy install nodejs - export PATH=$PATH:$GOPATH/bin - make release diff --git a/.github/lock.yml b/.github/lock.yml deleted file mode 100644 index 6beadcaf1109..000000000000 --- a/.github/lock.yml +++ /dev/null @@ -1,23 +0,0 @@ -# Configuration for Lock Threads - https://github.com/dessant/lock-threads-app - -# Number of days of inactivity before a closed issue or pull request is locked -daysUntilLock: 60 - -# Skip issues and pull requests created before a given timestamp. Timestamp must -# follow ISO 8601 (`YYYY-MM-DD`). `false` is disabled -skipCreatedBefore: false - -# Issues and pull requests with these labels will be ignored. -exemptLabels: [] - -# Label to add before locking, such as `outdated`. `false` is disabled -lockLabel: false - -# Comment to post before locking. -lockComment: > - This thread has been automatically locked since there has not been - any recent activity after it was closed. Please open a new issue for - related bugs and link to relevant comments in this thread. - -# Assign `resolved` as the reason for locking. Set to `false` to disable -setLockReason: true diff --git a/.github/workflows/lock.yml b/.github/workflows/lock.yml new file mode 100644 index 000000000000..2e132b95fed8 --- /dev/null +++ b/.github/workflows/lock.yml @@ -0,0 +1,21 @@ +name: 'Lock Threads' + +on: + schedule: + - cron: '0 0 * * *' # Run once a day + workflow_dispatch: + +permissions: + issues: write + pull-requests: write + +concurrency: + group: lock + +jobs: + action: + runs-on: ubuntu-latest + steps: + - uses: dessant/lock-threads@v4 + with: + issue-inactive-days: 45 diff --git a/.github/workflows/pull-compliance.yml b/.github/workflows/pull-compliance.yml index 1239b9caa7bf..94ca850e80eb 100644 --- a/.github/workflows/pull-compliance.yml +++ b/.github/workflows/pull-compliance.yml @@ -2,6 +2,10 @@ name: "Pull: Compliance Tests" on: [pull_request] +concurrency: + group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }} + cancel-in-progress: true + jobs: lint_basic: runs-on: ubuntu-latest @@ -79,7 +83,7 @@ jobs: - name: setup node uses: actions/setup-node@v3 with: - node-version: 18 + node-version: 20 - name: deps-frontend run: make deps-frontend - name: lint frontend @@ -100,7 +104,7 @@ jobs: - name: setup node uses: actions/setup-node@v3 with: - node-version: 18 + node-version: 20 - name: deps-backend run: make deps-backend deps-tools - name: deps-frontend diff --git a/.github/workflows/pull-compliance_docs.yml b/.github/workflows/pull-compliance_docs.yml index 679e925515e2..c033b62711df 100644 --- a/.github/workflows/pull-compliance_docs.yml +++ b/.github/workflows/pull-compliance_docs.yml @@ -6,6 +6,10 @@ on: - "docs/**" - "*.md" +concurrency: + group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }} + cancel-in-progress: true + jobs: compliance-docs: runs-on: ubuntu-latest @@ -13,9 +17,9 @@ jobs: - name: checkout uses: actions/checkout@v3 - name: setup node - uses: actions/setup-node@v2 + uses: actions/setup-node@v3 with: - node-version: 18 + node-version: 20 - name: install dependencies run: make deps-frontend - name: lint markdown diff --git a/.github/workflows/pull-db_test.yml b/.github/workflows/pull-db_test.yml index 3cae4df03962..ce97bfcb2c0f 100644 --- a/.github/workflows/pull-db_test.yml +++ b/.github/workflows/pull-db_test.yml @@ -2,6 +2,10 @@ name: "Pull: Database Tests" on: [pull_request] +concurrency: + group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }} + cancel-in-progress: true + jobs: # PostgreSQL Tests db_pgsql_test: diff --git a/.github/workflows/pull-docker_dryrun.yml b/.github/workflows/pull-docker_dryrun.yml index 8e5acb3cee1b..f17d6014b606 100644 --- a/.github/workflows/pull-docker_dryrun.yml +++ b/.github/workflows/pull-docker_dryrun.yml @@ -2,6 +2,10 @@ name: "Pull: Docker Dry Run" on: [pull_request] +concurrency: + group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }} + cancel-in-progress: true + jobs: docker_dryrun: runs-on: ubuntu-latest diff --git a/.github/workflows/pull-e2e.yml b/.github/workflows/pull-e2e.yml index 2cd6bd0d6ac6..37fc94fd96ce 100644 --- a/.github/workflows/pull-e2e.yml +++ b/.github/workflows/pull-e2e.yml @@ -2,6 +2,10 @@ name: "Pull: E2E Tests" on: [pull_request] +concurrency: + group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }} + cancel-in-progress: true + jobs: e2e_tests: runs-on: ubuntu-latest @@ -15,7 +19,7 @@ jobs: - name: setup node uses: actions/setup-node@v3 with: - node-version: 18 + node-version: 20 - name: build run: make deps-frontend frontend deps-backend - name: Install playwright browsers diff --git a/cmd/actions.go b/cmd/actions.go index 66ad336da508..346de5b21a6f 100644 --- a/cmd/actions.go +++ b/cmd/actions.go @@ -42,8 +42,7 @@ func runGenerateActionsRunnerToken(c *cli.Context) error { ctx, cancel := installSignals() defer cancel() - setting.InitProviderFromExistingFile() - setting.LoadCommonSettings() + setting.Init(&setting.Options{}) scope := c.String("scope") diff --git a/cmd/cmd.go b/cmd/cmd.go index 18d5db3987bd..cf2d9ef89e83 100644 --- a/cmd/cmd.go +++ b/cmd/cmd.go @@ -57,8 +57,7 @@ func confirm() (bool, error) { } func initDB(ctx context.Context) error { - setting.InitProviderFromExistingFile() - setting.LoadCommonSettings() + setting.Init(&setting.Options{}) setting.LoadDBSetting() setting.InitSQLLog(false) diff --git a/cmd/doctor.go b/cmd/doctor.go index e7baad60c1f9..65c028c5ed19 100644 --- a/cmd/doctor.go +++ b/cmd/doctor.go @@ -87,8 +87,7 @@ func runRecreateTable(ctx *cli.Context) error { golog.SetPrefix("") golog.SetOutput(log.NewLoggerAsWriter("INFO", log.GetLogger(log.DEFAULT))) - setting.InitProviderFromExistingFile() - setting.LoadCommonSettings() + setting.Init(&setting.Options{}) setting.LoadDBSetting() setting.Log.EnableXORMLog = ctx.Bool("debug") diff --git a/cmd/dump.go b/cmd/dump.go index 309bd01f6645..32ccc5566c8a 100644 --- a/cmd/dump.go +++ b/cmd/dump.go @@ -185,8 +185,7 @@ func runDump(ctx *cli.Context) error { } fileName += "." + outType } - setting.InitProviderFromExistingFile() - setting.LoadCommonSettings() + setting.Init(&setting.Options{}) // make sure we are logging to the console no matter what the configuration tells us do to // FIXME: don't use CfgProvider directly diff --git a/cmd/embedded.go b/cmd/embedded.go index cee8928ce08d..3f849bea0a26 100644 --- a/cmd/embedded.go +++ b/cmd/embedded.go @@ -106,8 +106,9 @@ func initEmbeddedExtractor(c *cli.Context) error { log.DelNamedLogger(log.DEFAULT) // Read configuration file - setting.InitProviderAllowEmpty() - setting.LoadCommonSettings() + setting.Init(&setting.Options{ + AllowEmpty: true, + }) patterns, err := compileCollectPatterns(c.Args()) if err != nil { diff --git a/cmd/mailer.go b/cmd/mailer.go index 50ba4b474110..74bae1ab68c7 100644 --- a/cmd/mailer.go +++ b/cmd/mailer.go @@ -16,8 +16,7 @@ func runSendMail(c *cli.Context) error { ctx, cancel := installSignals() defer cancel() - setting.InitProviderFromExistingFile() - setting.LoadCommonSettings() + setting.Init(&setting.Options{}) if err := argsSet(c, "title"); err != nil { return err diff --git a/cmd/main_test.go b/cmd/main_test.go index ba323af47217..6e20be69451c 100644 --- a/cmd/main_test.go +++ b/cmd/main_test.go @@ -7,14 +7,8 @@ import ( "testing" "code.gitea.io/gitea/models/unittest" - "code.gitea.io/gitea/modules/setting" ) -func init() { - setting.SetCustomPathAndConf("", "", "") - setting.InitProviderAndLoadCommonSettingsForTest() -} - func TestMain(m *testing.M) { unittest.MainTest(m, &unittest.TestOptions{ GiteaRootPath: "..", diff --git a/cmd/restore_repo.go b/cmd/restore_repo.go index 887b59bba9e1..5a7ede493975 100644 --- a/cmd/restore_repo.go +++ b/cmd/restore_repo.go @@ -51,8 +51,7 @@ func runRestoreRepository(c *cli.Context) error { ctx, cancel := installSignals() defer cancel() - setting.InitProviderFromExistingFile() - setting.LoadCommonSettings() + setting.Init(&setting.Options{}) var units []string if s := c.String("units"); s != "" { units = strings.Split(s, ",") diff --git a/cmd/serv.go b/cmd/serv.go index 72eb6370711e..a79f314d00b6 100644 --- a/cmd/serv.go +++ b/cmd/serv.go @@ -62,8 +62,7 @@ func setup(ctx context.Context, debug bool) { } else { _ = log.NewLogger(1000, "console", "console", `{"level":"fatal","stacktracelevel":"NONE","stderr":true}`) } - setting.InitProviderFromExistingFile() - setting.LoadCommonSettings() + setting.Init(&setting.Options{}) if debug { setting.RunMode = "dev" } diff --git a/cmd/web.go b/cmd/web.go index e451cf7dfa4f..3a01d07b05f5 100644 --- a/cmd/web.go +++ b/cmd/web.go @@ -177,8 +177,7 @@ func runWeb(ctx *cli.Context) error { log.Info("Global init") // Perform global initialization - setting.InitProviderFromExistingFile() - setting.LoadCommonSettings() + setting.Init(&setting.Options{}) routers.GlobalInitInstalled(graceful.GetManager().HammerContext()) // We check that AppDataPath exists here (it should have been created during installation) diff --git a/docs/content/doc/development/api-usage.en-us.md b/docs/content/doc/development/api-usage.en-us.md index fe334827c3ff..4f5304ac0e9d 100644 --- a/docs/content/doc/development/api-usage.en-us.md +++ b/docs/content/doc/development/api-usage.en-us.md @@ -119,7 +119,7 @@ curl -v "http://localhost/api/v1/repos/search?limit=1" < x-total-count: 5252 ``` -## API Guide: +## API Guide API Reference guide is auto-generated by swagger and available on: `https://gitea.your.host/api/swagger` diff --git a/go.mod b/go.mod index 58e90cc0fd3d..64266dd1ed92 100644 --- a/go.mod +++ b/go.mod @@ -290,8 +290,6 @@ replace github.com/hashicorp/go-version => github.com/6543/go-version v1.3.1 replace github.com/shurcooL/vfsgen => github.com/lunny/vfsgen v0.0.0-20220105142115-2c99e1ffdfa0 -replace github.com/blevesearch/zapx/v15 v15.3.6 => github.com/zeripath/zapx/v15 v15.3.6-alignment-fix - replace github.com/nektos/act => gitea.com/gitea/act v0.243.4 exclude github.com/gofrs/uuid v3.2.0+incompatible diff --git a/models/asymkey/main_test.go b/models/asymkey/main_test.go index 7f8657189fd1..701722be12ec 100644 --- a/models/asymkey/main_test.go +++ b/models/asymkey/main_test.go @@ -8,14 +8,8 @@ import ( "testing" "code.gitea.io/gitea/models/unittest" - "code.gitea.io/gitea/modules/setting" ) -func init() { - setting.SetCustomPathAndConf("", "", "") - setting.InitProviderAndLoadCommonSettingsForTest() -} - func TestMain(m *testing.M) { unittest.MainTest(m, &unittest.TestOptions{ GiteaRootPath: filepath.Join("..", ".."), diff --git a/models/dbfs/main_test.go b/models/dbfs/main_test.go index 9dce663eeab9..62db3592bed2 100644 --- a/models/dbfs/main_test.go +++ b/models/dbfs/main_test.go @@ -8,14 +8,8 @@ import ( "testing" "code.gitea.io/gitea/models/unittest" - "code.gitea.io/gitea/modules/setting" ) -func init() { - setting.SetCustomPathAndConf("", "", "") - setting.InitProviderAndLoadCommonSettingsForTest() -} - func TestMain(m *testing.M) { unittest.MainTest(m, &unittest.TestOptions{ GiteaRootPath: filepath.Join("..", ".."), diff --git a/models/issues/main_test.go b/models/issues/main_test.go index de84da30ecc0..9fbe294f7067 100644 --- a/models/issues/main_test.go +++ b/models/issues/main_test.go @@ -9,7 +9,6 @@ import ( issues_model "code.gitea.io/gitea/models/issues" "code.gitea.io/gitea/models/unittest" - "code.gitea.io/gitea/modules/setting" _ "code.gitea.io/gitea/models" _ "code.gitea.io/gitea/models/repo" @@ -18,11 +17,6 @@ import ( "github.com/stretchr/testify/assert" ) -func init() { - setting.SetCustomPathAndConf("", "", "") - setting.InitProviderAndLoadCommonSettingsForTest() -} - func TestFixturesAreConsistent(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) unittest.CheckConsistencyFor(t, diff --git a/models/main_test.go b/models/main_test.go index b5919bb28615..d490507649a3 100644 --- a/models/main_test.go +++ b/models/main_test.go @@ -11,18 +11,12 @@ import ( repo_model "code.gitea.io/gitea/models/repo" "code.gitea.io/gitea/models/unittest" user_model "code.gitea.io/gitea/models/user" - "code.gitea.io/gitea/modules/setting" _ "code.gitea.io/gitea/models/system" "github.com/stretchr/testify/assert" ) -func init() { - setting.SetCustomPathAndConf("", "", "") - setting.InitProviderAndLoadCommonSettingsForTest() -} - // TestFixturesAreConsistent assert that test fixtures are consistent func TestFixturesAreConsistent(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) diff --git a/models/migrations/base/tests.go b/models/migrations/base/tests.go index 14f374f1a7c3..124111f51fb2 100644 --- a/models/migrations/base/tests.go +++ b/models/migrations/base/tests.go @@ -150,7 +150,7 @@ func MainTest(m *testing.M) { setting.AppDataPath = tmpDataPath setting.SetCustomPathAndConf("", "", "") - setting.InitProviderAndLoadCommonSettingsForTest() + unittest.InitSettings() if err = git.InitFull(context.Background()); err != nil { fmt.Printf("Unable to InitFull: %v\n", err) os.Exit(1) diff --git a/models/unittest/testdb.go b/models/unittest/testdb.go index cff1489a7c7b..a5b126350d8e 100644 --- a/models/unittest/testdb.go +++ b/models/unittest/testdb.go @@ -6,12 +6,15 @@ package unittest import ( "context" "fmt" + "log" "os" "path/filepath" + "strings" "testing" "code.gitea.io/gitea/models/db" system_model "code.gitea.io/gitea/models/system" + "code.gitea.io/gitea/modules/auth/password/hash" "code.gitea.io/gitea/modules/base" "code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/setting" @@ -39,6 +42,22 @@ func fatalTestError(fmtStr string, args ...interface{}) { os.Exit(1) } +// InitSettings initializes config provider and load common setttings for tests +func InitSettings(extraConfigs ...string) { + setting.Init(&setting.Options{ + AllowEmpty: true, + ExtraConfig: strings.Join(extraConfigs, "\n"), + }) + + if err := setting.PrepareAppDataPath(); err != nil { + log.Fatalf("Can not prepare APP_DATA_PATH: %v", err) + } + // register the dummy hash algorithm function used in the test fixtures + _ = hash.Register("dummy", hash.NewDummyHasher) + + setting.PasswordHashAlgo, _ = hash.SetDefaultPasswordHashAlgorithm("dummy") +} + // TestOptions represents test options type TestOptions struct { GiteaRootPath string @@ -50,6 +69,9 @@ type TestOptions struct { // MainTest a reusable TestMain(..) function for unit tests that need to use a // test database. Creates the test database, and sets necessary settings. func MainTest(m *testing.M, testOpts *TestOptions) { + setting.SetCustomPathAndConf("", "", "") + InitSettings() + var err error giteaRoot = testOpts.GiteaRootPath diff --git a/modules/context/access_log.go b/modules/context/access_log.go index 64d204733b8a..b6468d139bf9 100644 --- a/modules/context/access_log.go +++ b/modules/context/access_log.go @@ -5,7 +5,6 @@ package context import ( "bytes" - "context" "fmt" "net" "net/http" @@ -13,8 +12,10 @@ import ( "text/template" "time" + user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/setting" + "code.gitea.io/gitea/modules/web/middleware" ) type routerLoggerOptions struct { @@ -26,8 +27,6 @@ type routerLoggerOptions struct { RequestID *string } -var signedUserNameStringPointerKey interface{} = "signedUserNameStringPointerKey" - const keyOfRequestIDInTemplate = ".RequestID" // According to: @@ -60,8 +59,6 @@ func AccessLogger() func(http.Handler) http.Handler { return func(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { start := time.Now() - identity := "-" - r := req.WithContext(context.WithValue(req.Context(), signedUserNameStringPointerKey, &identity)) var requestID string if needRequestID { @@ -73,9 +70,14 @@ func AccessLogger() func(http.Handler) http.Handler { reqHost = req.RemoteAddr } - next.ServeHTTP(w, r) + next.ServeHTTP(w, req) rw := w.(ResponseWriter) + identity := "-" + data := middleware.GetContextData(req.Context()) + if signedUser, ok := data[middleware.ContextDataKeySignedUser].(*user_model.User); ok { + identity = signedUser.Name + } buf := bytes.NewBuffer([]byte{}) err = logTemplate.Execute(buf, routerLoggerOptions{ req: req, diff --git a/modules/context/api.go b/modules/context/api.go index ae245ec1cb62..e263dcbe8dea 100644 --- a/modules/context/api.go +++ b/modules/context/api.go @@ -222,7 +222,7 @@ func APIContexter() func(http.Handler) http.Handler { ctx := APIContext{ Context: &Context{ Resp: NewResponse(w), - Data: map[string]interface{}{}, + Data: middleware.GetContextData(req.Context()), Locale: locale, Cache: cache.GetCache(), Repo: &Repository{ @@ -250,17 +250,6 @@ func APIContexter() func(http.Handler) http.Handler { ctx.Data["Context"] = &ctx next.ServeHTTP(ctx.Resp, ctx.Req) - - // Handle adding signedUserName to the context for the AccessLogger - usernameInterface := ctx.Data["SignedUserName"] - identityPtrInterface := ctx.Req.Context().Value(signedUserNameStringPointerKey) - if usernameInterface != nil && identityPtrInterface != nil { - username := usernameInterface.(string) - identityPtr := identityPtrInterface.(*string) - if identityPtr != nil && username != "" { - *identityPtr = username - } - } }) } } diff --git a/modules/context/context.go b/modules/context/context.go index cd7fcebe55da..d73a26e5b6f3 100644 --- a/modules/context/context.go +++ b/modules/context/context.go @@ -55,7 +55,7 @@ type Render interface { type Context struct { Resp ResponseWriter Req *http.Request - Data map[string]interface{} // data used by MVC templates + Data middleware.ContextData // data used by MVC templates PageData map[string]interface{} // data used by JavaScript modules in one page, it's `window.config.pageData` Render Render translation.Locale @@ -97,7 +97,7 @@ func (ctx *Context) TrHTMLEscapeArgs(msg string, args ...string) string { } // GetData returns the data -func (ctx *Context) GetData() map[string]interface{} { +func (ctx *Context) GetData() middleware.ContextData { return ctx.Data } @@ -219,6 +219,7 @@ const tplStatus500 base.TplName = "status/500" // HTML calls Context.HTML and renders the template to HTTP response func (ctx *Context) HTML(status int, name base.TplName) { log.Debug("Template: %s", name) + tmplStartTime := time.Now() if !setting.IsProd { ctx.Data["TemplateName"] = name @@ -226,13 +227,19 @@ func (ctx *Context) HTML(status int, name base.TplName) { ctx.Data["TemplateLoadTimes"] = func() string { return strconv.FormatInt(time.Since(tmplStartTime).Nanoseconds()/1e6, 10) + "ms" } - if err := ctx.Render.HTML(ctx.Resp, status, string(name), templates.BaseVars().Merge(ctx.Data)); err != nil { - if status == http.StatusInternalServerError && name == tplStatus500 { - ctx.PlainText(http.StatusInternalServerError, "Unable to find HTML templates, the template system is not initialized, or Gitea can't find your template files.") - return - } + + err := ctx.Render.HTML(ctx.Resp, status, string(name), ctx.Data) + if err == nil { + return + } + + // if rendering fails, show error page + if name != tplStatus500 { err = fmt.Errorf("failed to render template: %s, error: %s", name, templates.HandleTemplateRenderingError(err)) - ctx.ServerError("Render failed", err) + ctx.ServerError("Render failed", err) // show the 500 error page + } else { + ctx.PlainText(http.StatusInternalServerError, "Unable to render status/500 page, the template system is broken, or Gitea can't find your template files.") + return } } @@ -676,7 +683,7 @@ func getCsrfOpts() CsrfOptions { } // Contexter initializes a classic context for a request. -func Contexter(ctx context.Context) func(next http.Handler) http.Handler { +func Contexter() func(next http.Handler) http.Handler { rnd := templates.HTMLRenderer() csrfOpts := getCsrfOpts() if !setting.IsProd { @@ -684,34 +691,30 @@ func Contexter(ctx context.Context) func(next http.Handler) http.Handler { } return func(next http.Handler) http.Handler { return http.HandlerFunc(func(resp http.ResponseWriter, req *http.Request) { - locale := middleware.Locale(resp, req) - startTime := time.Now() - link := setting.AppSubURL + strings.TrimSuffix(req.URL.EscapedPath(), "/") - ctx := Context{ Resp: NewResponse(resp), Cache: mc.GetCache(), - Locale: locale, - Link: link, + Locale: middleware.Locale(resp, req), + Link: setting.AppSubURL + strings.TrimSuffix(req.URL.EscapedPath(), "/"), Render: rnd, Session: session.GetSession(req), Repo: &Repository{ PullRequest: &PullRequest{}, }, - Org: &Organization{}, - Data: map[string]interface{}{ - "CurrentURL": setting.AppSubURL + req.URL.RequestURI(), - "PageStartTime": startTime, - "Link": link, - "RunModeIsProd": setting.IsProd, - }, + Org: &Organization{}, + Data: middleware.GetContextData(req.Context()), } defer ctx.Close() + ctx.Data.MergeFrom(middleware.CommonTemplateContextData()) + ctx.Data["Context"] = &ctx + ctx.Data["CurrentURL"] = setting.AppSubURL + req.URL.RequestURI() + ctx.Data["Link"] = ctx.Link + ctx.Data["locale"] = ctx.Locale + // PageData is passed by reference, and it will be rendered to `window.config.pageData` in `head.tmpl` for JavaScript modules - ctx.PageData = map[string]interface{}{} + ctx.PageData = map[string]any{} ctx.Data["PageData"] = ctx.PageData - ctx.Data["Context"] = &ctx ctx.Req = WithContext(req, &ctx) ctx.Csrf = PrepareCSRFProtector(csrfOpts, &ctx) @@ -755,16 +758,6 @@ func Contexter(ctx context.Context) func(next http.Handler) http.Handler { ctx.Data["CsrfTokenHtml"] = template.HTML(``) // FIXME: do we really always need these setting? There should be someway to have to avoid having to always set these - ctx.Data["IsLandingPageHome"] = setting.LandingPageURL == setting.LandingPageHome - ctx.Data["IsLandingPageExplore"] = setting.LandingPageURL == setting.LandingPageExplore - ctx.Data["IsLandingPageOrganizations"] = setting.LandingPageURL == setting.LandingPageOrganizations - - ctx.Data["ShowRegistrationButton"] = setting.Service.ShowRegistrationButton - ctx.Data["ShowMilestonesDashboardPage"] = setting.Service.ShowMilestonesDashboardPage - ctx.Data["ShowFooterVersion"] = setting.Other.ShowFooterVersion - - ctx.Data["EnableSwagger"] = setting.API.EnableSwagger - ctx.Data["EnableOpenIDSignIn"] = setting.Service.EnableOpenIDSignIn ctx.Data["DisableMigrations"] = setting.Repository.DisableMigrations ctx.Data["DisableStars"] = setting.Repository.DisableStars ctx.Data["EnableActions"] = setting.Actions.Enabled @@ -777,21 +770,9 @@ func Contexter(ctx context.Context) func(next http.Handler) http.Handler { ctx.Data["UnitProjectsGlobalDisabled"] = unit.TypeProjects.UnitGlobalDisabled() ctx.Data["UnitActionsGlobalDisabled"] = unit.TypeActions.UnitGlobalDisabled() - ctx.Data["locale"] = locale ctx.Data["AllLangs"] = translation.AllLangs() next.ServeHTTP(ctx.Resp, ctx.Req) - - // Handle adding signedUserName to the context for the AccessLogger - usernameInterface := ctx.Data["SignedUserName"] - identityPtrInterface := ctx.Req.Context().Value(signedUserNameStringPointerKey) - if usernameInterface != nil && identityPtrInterface != nil { - username := usernameInterface.(string) - identityPtr := identityPtrInterface.(*string) - if identityPtr != nil && username != "" { - *identityPtr = username - } - } }) } } diff --git a/modules/context/package.go b/modules/context/package.go index 6c418b316466..fe5bdac19d67 100644 --- a/modules/context/package.go +++ b/modules/context/package.go @@ -16,6 +16,7 @@ import ( "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/structs" "code.gitea.io/gitea/modules/templates" + "code.gitea.io/gitea/modules/web/middleware" ) // Package contains owner, access mode and optional the package descriptor @@ -136,7 +137,7 @@ func PackageContexter(ctx gocontext.Context) func(next http.Handler) http.Handle return http.HandlerFunc(func(resp http.ResponseWriter, req *http.Request) { ctx := Context{ Resp: NewResponse(resp), - Data: map[string]interface{}{}, + Data: middleware.GetContextData(req.Context()), Render: rnd, } defer ctx.Close() diff --git a/modules/context/private.go b/modules/context/private.go index 24f50fa4713e..f621dd68390f 100644 --- a/modules/context/private.go +++ b/modules/context/private.go @@ -11,6 +11,7 @@ import ( "code.gitea.io/gitea/modules/graceful" "code.gitea.io/gitea/modules/process" + "code.gitea.io/gitea/modules/web/middleware" ) // PrivateContext represents a context for private routes @@ -62,7 +63,7 @@ func PrivateContexter() func(http.Handler) http.Handler { ctx := &PrivateContext{ Context: &Context{ Resp: NewResponse(w), - Data: map[string]interface{}{}, + Data: middleware.GetContextData(req.Context()), }, } defer ctx.Close() diff --git a/modules/doctor/doctor.go b/modules/doctor/doctor.go index b23805bc4c96..32eb5938c307 100644 --- a/modules/doctor/doctor.go +++ b/modules/doctor/doctor.go @@ -44,8 +44,7 @@ func (w *wrappedLevelLogger) Log(skip int, level log.Level, format string, v ... } func initDBDisableConsole(ctx context.Context, disableConsole bool) error { - setting.InitProviderFromExistingFile() - setting.LoadCommonSettings() + setting.Init(&setting.Options{}) setting.LoadDBSetting() setting.InitSQLLog(disableConsole) if err := db.InitEngine(ctx); err != nil { diff --git a/modules/doctor/paths.go b/modules/doctor/paths.go index 1558efc25b90..957152349c25 100644 --- a/modules/doctor/paths.go +++ b/modules/doctor/paths.go @@ -66,8 +66,7 @@ func checkConfigurationFiles(ctx context.Context, logger log.Logger, autofix boo return err } - setting.InitProviderFromExistingFile() - setting.LoadCommonSettings() + setting.Init(&setting.Options{}) configurationFiles := []configurationFile{ {"Configuration File Path", setting.CustomConf, false, true, false}, diff --git a/modules/git/repo.go b/modules/git/repo.go index 3637aa47c458..61930ab31db9 100644 --- a/modules/git/repo.go +++ b/modules/git/repo.go @@ -244,35 +244,28 @@ type DivergeObject struct { Behind int } -func checkDivergence(ctx context.Context, repoPath, baseBranch, targetBranch string) (int, error) { - branches := fmt.Sprintf("%s..%s", baseBranch, targetBranch) - cmd := NewCommand(ctx, "rev-list", "--count").AddDynamicArguments(branches) +// GetDivergingCommits returns the number of commits a targetBranch is ahead or behind a baseBranch +func GetDivergingCommits(ctx context.Context, repoPath, baseBranch, targetBranch string) (do DivergeObject, err error) { + cmd := NewCommand(ctx, "rev-list", "--count", "--left-right"). + AddDynamicArguments(baseBranch + "..." + targetBranch) stdout, _, err := cmd.RunStdString(&RunOpts{Dir: repoPath}) if err != nil { - return -1, err + return do, err } - outInteger, errInteger := strconv.Atoi(strings.Trim(stdout, "\n")) - if errInteger != nil { - return -1, errInteger + left, right, found := strings.Cut(strings.Trim(stdout, "\n"), "\t") + if !found { + return do, fmt.Errorf("git rev-list output is missing a tab: %q", stdout) } - return outInteger, nil -} -// GetDivergingCommits returns the number of commits a targetBranch is ahead or behind a baseBranch -func GetDivergingCommits(ctx context.Context, repoPath, baseBranch, targetBranch string) (DivergeObject, error) { - // $(git rev-list --count master..feature) commits ahead of master - ahead, errorAhead := checkDivergence(ctx, repoPath, baseBranch, targetBranch) - if errorAhead != nil { - return DivergeObject{}, errorAhead + do.Behind, err = strconv.Atoi(left) + if err != nil { + return do, err } - - // $(git rev-list --count feature..master) commits behind master - behind, errorBehind := checkDivergence(ctx, repoPath, targetBranch, baseBranch) - if errorBehind != nil { - return DivergeObject{}, errorBehind + do.Ahead, err = strconv.Atoi(right) + if err != nil { + return do, err } - - return DivergeObject{ahead, behind}, nil + return do, nil } // CreateBundle create bundle content to the target path diff --git a/modules/git/repo_test.go b/modules/git/repo_test.go index 044b9d406502..9db78153a102 100644 --- a/modules/git/repo_test.go +++ b/modules/git/repo_test.go @@ -4,6 +4,7 @@ package git import ( + "context" "path/filepath" "testing" @@ -29,3 +30,27 @@ func TestRepoIsEmpty(t *testing.T) { assert.NoError(t, err) assert.True(t, isEmpty) } + +func TestRepoGetDivergingCommits(t *testing.T) { + bareRepo1Path := filepath.Join(testReposDir, "repo1_bare") + do, err := GetDivergingCommits(context.Background(), bareRepo1Path, "master", "branch2") + assert.NoError(t, err) + assert.Equal(t, DivergeObject{ + Ahead: 1, + Behind: 5, + }, do) + + do, err = GetDivergingCommits(context.Background(), bareRepo1Path, "master", "master") + assert.NoError(t, err) + assert.Equal(t, DivergeObject{ + Ahead: 0, + Behind: 0, + }, do) + + do, err = GetDivergingCommits(context.Background(), bareRepo1Path, "master", "test") + assert.NoError(t, err) + assert.Equal(t, DivergeObject{ + Ahead: 0, + Behind: 2, + }, do) +} diff --git a/modules/markup/html_test.go b/modules/markup/html_test.go index cb1216ec946e..5e5e4fecbbb7 100644 --- a/modules/markup/html_test.go +++ b/modules/markup/html_test.go @@ -28,8 +28,9 @@ var localMetas = map[string]string{ } func TestMain(m *testing.M) { - setting.InitProviderAllowEmpty() - setting.LoadCommonSettings() + setting.Init(&setting.Options{ + AllowEmpty: true, + }) if err := git.InitSimple(context.Background()); err != nil { log.Fatal("git init failed, err: %v", err) } diff --git a/modules/markup/markdown/markdown_test.go b/modules/markup/markdown/markdown_test.go index 0c7650a5ffab..e81869d7a443 100644 --- a/modules/markup/markdown/markdown_test.go +++ b/modules/markup/markdown/markdown_test.go @@ -33,8 +33,9 @@ var localMetas = map[string]string{ } func TestMain(m *testing.M) { - setting.InitProviderAllowEmpty() - setting.LoadCommonSettings() + setting.Init(&setting.Options{ + AllowEmpty: true, + }) if err := git.InitSimple(context.Background()); err != nil { log.Fatal("git init failed, err: %v", err) } diff --git a/modules/setting/config_provider.go b/modules/setting/config_provider.go index 92c8c97fe952..168595829863 100644 --- a/modules/setting/config_provider.go +++ b/modules/setting/config_provider.go @@ -35,10 +35,9 @@ type ConfigProvider interface { } type iniFileConfigProvider struct { + opts *Options *ini.File - filepath string // the ini file path - newFile bool // whether the file has not existed previously - allowEmpty bool // whether not finding configuration files is allowed (only true for the tests) + newFile bool // whether the file has not existed previously } // NewEmptyConfigProvider create a new empty config provider @@ -66,41 +65,47 @@ func newConfigProviderFromData(configContent string) (ConfigProvider, error) { }, nil } +type Options struct { + CustomConf string // the ini file path + AllowEmpty bool // whether not finding configuration files is allowed (only true for the tests) + ExtraConfig string + DisableLoadCommonSettings bool +} + // newConfigProviderFromFile load configuration from file. // NOTE: do not print any log except error. -func newConfigProviderFromFile(customConf string, allowEmpty bool, extraConfig string) (*iniFileConfigProvider, error) { +func newConfigProviderFromFile(opts *Options) (*iniFileConfigProvider, error) { cfg := ini.Empty() newFile := true - if customConf != "" { - isFile, err := util.IsFile(customConf) + if opts.CustomConf != "" { + isFile, err := util.IsFile(opts.CustomConf) if err != nil { - return nil, fmt.Errorf("unable to check if %s is a file. Error: %v", customConf, err) + return nil, fmt.Errorf("unable to check if %s is a file. Error: %v", opts.CustomConf, err) } if isFile { - if err := cfg.Append(customConf); err != nil { - return nil, fmt.Errorf("failed to load custom conf '%s': %v", customConf, err) + if err := cfg.Append(opts.CustomConf); err != nil { + return nil, fmt.Errorf("failed to load custom conf '%s': %v", opts.CustomConf, err) } newFile = false } } - if newFile && !allowEmpty { + if newFile && !opts.AllowEmpty { return nil, fmt.Errorf("unable to find configuration file: %q, please ensure you are running in the correct environment or set the correct configuration file with -c", CustomConf) } - if extraConfig != "" { - if err := cfg.Append([]byte(extraConfig)); err != nil { + if opts.ExtraConfig != "" { + if err := cfg.Append([]byte(opts.ExtraConfig)); err != nil { return nil, fmt.Errorf("unable to append more config: %v", err) } } cfg.NameMapper = ini.SnackCase return &iniFileConfigProvider{ - File: cfg, - filepath: customConf, - newFile: newFile, - allowEmpty: allowEmpty, + opts: opts, + File: cfg, + newFile: newFile, }, nil } @@ -123,8 +128,8 @@ func (p *iniFileConfigProvider) DeleteSection(name string) error { // Save save the content into file func (p *iniFileConfigProvider) Save() error { - if p.filepath == "" { - if !p.allowEmpty { + if p.opts.CustomConf == "" { + if !p.opts.AllowEmpty { return fmt.Errorf("custom config path must not be empty") } return nil @@ -135,8 +140,8 @@ func (p *iniFileConfigProvider) Save() error { return fmt.Errorf("failed to create '%s': %v", CustomConf, err) } } - if err := p.SaveTo(p.filepath); err != nil { - return fmt.Errorf("failed to save '%s': %v", p.filepath, err) + if err := p.SaveTo(p.opts.CustomConf); err != nil { + return fmt.Errorf("failed to save '%s': %v", p.opts.CustomConf, err) } // Change permissions to be more restrictive diff --git a/modules/setting/setting.go b/modules/setting/setting.go index 9ab55e91c531..b085a7b32149 100644 --- a/modules/setting/setting.go +++ b/modules/setting/setting.go @@ -15,7 +15,6 @@ import ( "strings" "time" - "code.gitea.io/gitea/modules/auth/password/hash" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/user" ) @@ -203,44 +202,18 @@ func PrepareAppDataPath() error { return nil } -// InitProviderFromExistingFile initializes config provider from an existing config file (app.ini) -func InitProviderFromExistingFile() { - var err error - CfgProvider, err = newConfigProviderFromFile(CustomConf, false, "") - if err != nil { - log.Fatal("InitProviderFromExistingFile: %v", err) - } -} - -// InitProviderAllowEmpty initializes config provider from file, it's also fine that if the config file (app.ini) doesn't exist -func InitProviderAllowEmpty() { - var err error - CfgProvider, err = newConfigProviderFromFile(CustomConf, true, "") - if err != nil { - log.Fatal("InitProviderAllowEmpty: %v", err) +func Init(opts *Options) { + if opts.CustomConf == "" { + opts.CustomConf = CustomConf } -} - -// InitProviderAndLoadCommonSettingsForTest initializes config provider and load common setttings for tests -func InitProviderAndLoadCommonSettingsForTest(extraConfigs ...string) { var err error - CfgProvider, err = newConfigProviderFromFile(CustomConf, true, strings.Join(extraConfigs, "\n")) + CfgProvider, err = newConfigProviderFromFile(opts) if err != nil { - log.Fatal("InitProviderAndLoadCommonSettingsForTest: %v", err) + log.Fatal("Init[%v]: %v", opts, err) } - loadCommonSettingsFrom(CfgProvider) - if err := PrepareAppDataPath(); err != nil { - log.Fatal("Can not prepare APP_DATA_PATH: %v", err) + if !opts.DisableLoadCommonSettings { + loadCommonSettingsFrom(CfgProvider) } - // register the dummy hash algorithm function used in the test fixtures - _ = hash.Register("dummy", hash.NewDummyHasher) - - PasswordHashAlgo, _ = hash.SetDefaultPasswordHashAlgorithm("dummy") -} - -// LoadCommonSettings loads common configurations from a configuration provider. -func LoadCommonSettings() { - loadCommonSettingsFrom(CfgProvider) } // loadCommonSettingsFrom loads common configurations from a configuration provider. diff --git a/modules/templates/base.go b/modules/templates/base.go index 4254a569764e..ef28cc03f45d 100644 --- a/modules/templates/base.go +++ b/modules/templates/base.go @@ -5,43 +5,12 @@ package templates import ( "strings" - "time" "code.gitea.io/gitea/modules/assetfs" "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/util" ) -// Vars represents variables to be render in golang templates -type Vars map[string]interface{} - -// Merge merges another vars to the current, another Vars will override the current -func (vars Vars) Merge(another map[string]interface{}) Vars { - for k, v := range another { - vars[k] = v - } - return vars -} - -// BaseVars returns all basic vars -func BaseVars() Vars { - startTime := time.Now() - return map[string]interface{}{ - "IsLandingPageHome": setting.LandingPageURL == setting.LandingPageHome, - "IsLandingPageExplore": setting.LandingPageURL == setting.LandingPageExplore, - "IsLandingPageOrganizations": setting.LandingPageURL == setting.LandingPageOrganizations, - - "ShowRegistrationButton": setting.Service.ShowRegistrationButton, - "ShowMilestonesDashboardPage": setting.Service.ShowMilestonesDashboardPage, - "ShowFooterVersion": setting.Other.ShowFooterVersion, - "DisableDownloadSourceArchives": setting.Repository.DisableDownloadSourceArchives, - - "EnableSwagger": setting.API.EnableSwagger, - "EnableOpenIDSignIn": setting.Service.EnableOpenIDSignIn, - "PageStartTime": startTime, - } -} - func AssetFS() *assetfs.LayeredFS { return assetfs.Layered(CustomAssets(), BuiltinAssets()) } diff --git a/modules/test/context_tests.go b/modules/test/context_tests.go index 4af76512505c..35dd920bb92a 100644 --- a/modules/test/context_tests.go +++ b/modules/test/context_tests.go @@ -30,7 +30,7 @@ func MockContext(t *testing.T, path string) *context.Context { resp := &mockResponseWriter{} ctx := context.Context{ Render: &mockRender{}, - Data: make(map[string]interface{}), + Data: make(middleware.ContextData), Flash: &middleware.Flash{ Values: make(url.Values), }, diff --git a/modules/web/middleware/data.go b/modules/web/middleware/data.go index 43189940ee20..c1f0516d7d2f 100644 --- a/modules/web/middleware/data.go +++ b/modules/web/middleware/data.go @@ -3,7 +3,63 @@ package middleware -// DataStore represents a data store -type DataStore interface { - GetData() map[string]interface{} +import ( + "context" + "time" + + "code.gitea.io/gitea/modules/setting" +) + +// ContextDataStore represents a data store +type ContextDataStore interface { + GetData() ContextData +} + +type ContextData map[string]any + +func (ds ContextData) GetData() map[string]any { + return ds +} + +func (ds ContextData) MergeFrom(other ContextData) ContextData { + for k, v := range other { + ds[k] = v + } + return ds +} + +const ContextDataKeySignedUser = "SignedUser" + +type contextDataKeyType struct{} + +var contextDataKey contextDataKeyType + +func WithContextData(c context.Context) context.Context { + return context.WithValue(c, contextDataKey, make(ContextData, 10)) +} + +func GetContextData(c context.Context) ContextData { + if ds, ok := c.Value(contextDataKey).(ContextData); ok { + return ds + } + return nil +} + +func CommonTemplateContextData() ContextData { + return ContextData{ + "IsLandingPageHome": setting.LandingPageURL == setting.LandingPageHome, + "IsLandingPageExplore": setting.LandingPageURL == setting.LandingPageExplore, + "IsLandingPageOrganizations": setting.LandingPageURL == setting.LandingPageOrganizations, + + "ShowRegistrationButton": setting.Service.ShowRegistrationButton, + "ShowMilestonesDashboardPage": setting.Service.ShowMilestonesDashboardPage, + "ShowFooterVersion": setting.Other.ShowFooterVersion, + "DisableDownloadSourceArchives": setting.Repository.DisableDownloadSourceArchives, + + "EnableSwagger": setting.API.EnableSwagger, + "EnableOpenIDSignIn": setting.Service.EnableOpenIDSignIn, + "PageStartTime": time.Now(), + + "RunModeIsProd": setting.IsProd, + } } diff --git a/modules/web/middleware/flash.go b/modules/web/middleware/flash.go index f2d7cc692d28..fa29ddeffc79 100644 --- a/modules/web/middleware/flash.go +++ b/modules/web/middleware/flash.go @@ -18,7 +18,7 @@ var FlashNow bool // Flash represents a one time data transfer between two requests. type Flash struct { - DataStore + DataStore ContextDataStore url.Values ErrorMsg, WarningMsg, InfoMsg, SuccessMsg string } @@ -34,7 +34,7 @@ func (f *Flash) set(name, msg string, current ...bool) { } if isShow { - f.GetData()["Flash"] = f + f.DataStore.GetData()["Flash"] = f } else { f.Set(name, msg) } diff --git a/modules/web/middleware/request.go b/modules/web/middleware/request.go index 34add27214b8..0bb155df7034 100644 --- a/modules/web/middleware/request.go +++ b/modules/web/middleware/request.go @@ -12,8 +12,3 @@ import ( func IsAPIPath(req *http.Request) bool { return strings.HasPrefix(req.URL.Path, "/api/") } - -// IsInternalPath returns true if the specified URL is an internal API path -func IsInternalPath(req *http.Request) bool { - return strings.HasPrefix(req.URL.Path, "/api/internal/") -} diff --git a/modules/web/route.go b/modules/web/route.go index 6fd60f4ca739..d801f1025c94 100644 --- a/modules/web/route.go +++ b/modules/web/route.go @@ -25,12 +25,12 @@ func Bind[T any](_ T) any { } // SetForm set the form object -func SetForm(data middleware.DataStore, obj interface{}) { +func SetForm(data middleware.ContextDataStore, obj interface{}) { data.GetData()["__form"] = obj } // GetForm returns the validate form information -func GetForm(data middleware.DataStore) interface{} { +func GetForm(data middleware.ContextDataStore) interface{} { return data.GetData()["__form"] } diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini index 180fd1c18d0e..ee8ec1a3adca 100644 --- a/options/locale/locale_en-US.ini +++ b/options/locale/locale_en-US.ini @@ -1181,7 +1181,7 @@ editor.filename_is_invalid = The filename is invalid: "%s". editor.branch_does_not_exist = Branch "%s" does not exist in this repository. 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.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. @@ -1904,6 +1904,7 @@ settings.sync_mirror = Synchronize Now settings.mirror_sync_in_progress = Mirror synchronization is in progress. Check back in a minute. settings.site = Website settings.update_settings = Update Settings +settings.update_mirror_settings = Update Mirror Settings settings.branches.switch_default_branch = Switch Default Branch settings.branches.update_default_branch = Update Default Branch settings.branches.add_new_rule = Add New Rule @@ -2411,6 +2412,7 @@ branch.included_desc = This branch is part of the default branch branch.included = Included branch.create_new_branch = Create branch from branch: branch.confirm_create_branch = Create branch +branch.warning_rename_default_branch = You are renaming the default branch. branch.rename_branch_to = Rename "%s" to: branch.confirm_rename_branch = Rename branch branch.create_branch_operation = Create branch diff --git a/options/locale/locale_zh-CN.ini b/options/locale/locale_zh-CN.ini index f1921828349c..d4360fe49a2e 100644 --- a/options/locale/locale_zh-CN.ini +++ b/options/locale/locale_zh-CN.ini @@ -119,8 +119,26 @@ footer.software=关于软件 footer.links=链接 [heatmap] +number_of_contributions_in_the_last_12_months=一年内 %s 次贡献 +no_contributions=目前还没有贡献。 +less=更少的 +more=更多的 [editor] +buttons.heading.tooltip=添加标题 +buttons.bold.tooltip=添加粗体文本 +buttons.italic.tooltip=添加斜体文本 +buttons.quote.tooltip=引用文本 +buttons.code.tooltip=添加代码 +buttons.link.tooltip=添加链接 +buttons.list.unordered.tooltip=添加待办清单 +buttons.list.ordered.tooltip=添加编号列表 +buttons.list.task.tooltip=添加任务列表 +buttons.mention.tooltip=提及用户或团队 +buttons.ref.tooltip=引用一个问题或拉取请求 +buttons.switch_to_legacy.tooltip=使用旧版编辑器 +buttons.enable_monospace_font=启用等宽字体 +buttons.disable_monospace_font=禁用等宽字体 [filter] string.asc=A - Z @@ -234,6 +252,7 @@ install_btn_confirm=立即安装 test_git_failed=无法识别 'git' 命令:%v sqlite3_not_available=您所使用的发行版不支持 SQLite3,请从 %s 下载官方构建版,而不是 gobuild 版本。 invalid_db_setting=数据库设置无效: %v +invalid_db_table=数据库表 '%s' 无效: %v invalid_repo_path=仓库根目录设置无效:%v invalid_app_data_path=应用数据路径无效: %v run_user_not_match=运行用户名不是当前的用户名:%s -> %s @@ -299,6 +318,7 @@ repo_no_results=未找到匹配的仓库。 user_no_results=未找到匹配的用户。 org_no_results=未找到匹配的组织。 code_no_results=未找到与搜索字词匹配的源代码。 +code_search_results=“%s” 的搜索结果是 code_last_indexed_at=最后索引于 %s relevant_repositories_tooltip=派生的仓库,以及缺少主题、图标和描述的仓库将被隐藏。 relevant_repositories=只显示相关的仓库, 显示未过滤结果。 @@ -442,6 +462,7 @@ team_invite.text_3=注意:这是发送给 %[1]s 的邀请。如果您未曾收 [modal] yes=确认操作 no=取消操作 +confirm=确认 cancel=取消 modify=更新 @@ -476,6 +497,8 @@ size_error=长度必须为 %s。 min_size_error=长度最小为 %s 个字符。 max_size_error=长度最大为 %s 个字符。 email_error=不是一个有效的邮箱地址。 +url_error=`'%s' 不是一个有效的 URL。` +include_error=`必须包含子字符串 "%s"。` glob_pattern_error=`匹配模式无效:%s.` regex_pattern_error=`正则表达式无效:%s.` username_error=` 只能包含字母数字字符('0-9','a-z','A-Z'), 破折号 ('-'), 下划线 ('_') 和点 ('.'). 不能以非字母数字字符开头或结尾,并且不允许连续的非字母数字字符。` @@ -500,6 +523,7 @@ team_name_been_taken=团队名称已被使用。 team_no_units_error=至少选择一项仓库单元。 email_been_used=该电子邮件地址已在使用中。 email_invalid=此邮箱地址无效。 +openid_been_used=OpenID 地址 "%s" 已被使用。 username_password_incorrect=用户名或密码不正确。 password_complexity=密码未达到复杂程度要求: password_lowercase_one=至少一个小写字符 @@ -548,7 +572,12 @@ unfollow=取消关注 heatmap.loading=正在加载热图... user_bio=简历 disabled_public_activity=该用户已隐藏活动记录。 +email_visibility.limited=所有已认证用户均可看到您的电子邮件地址 +email_visibility.private=只有你本人和管理员可以看到你的电子邮件地址 +form.name_reserved=用户名 "%s" 被保留。 +form.name_pattern_not_allowed=用户名中不允许使用 "%s" 格式。 +form.name_chars_not_allowed=用户名 "%s" 包含无效字符。 [settings] profile=个人信息 @@ -579,6 +608,7 @@ location=所在地区 update_theme=更新主题 update_profile=更新信息 update_language=更新语言 +update_language_not_found=语言 %s 不可用。 update_language_success=语言已更新。 update_profile_success=您的资料信息已经更新 change_username=您的用户名已更改。 @@ -589,6 +619,9 @@ cancel=取消操作 language=界面语言 ui=主题 hidden_comment_types=隐藏的评论类型 +hidden_comment_types_description=此处选中的注释类型不会显示在问题页面中。比如,勾选”标签“删除所有 " 添加/删除的