diff --git a/Makefile b/Makefile index 2e2c86376b3b0..3e2f1524e4052 100644 --- a/Makefile +++ b/Makefile @@ -17,7 +17,6 @@ else DIST := dist DIST_DIRS := $(DIST)/binaries $(DIST)/release IMPORT := code.gitea.io/gitea -export GO111MODULE=on GO ?= go SHASUM ?= shasum -a 256 @@ -363,7 +362,7 @@ test\#%: coverage: grep '^\(mode: .*\)\|\(.*:[0-9]\+\.[0-9]\+,[0-9]\+\.[0-9]\+ [0-9]\+ [0-9]\+\)$$' coverage.out > coverage-bodged.out grep '^\(mode: .*\)\|\(.*:[0-9]\+\.[0-9]\+,[0-9]\+\.[0-9]\+ [0-9]\+ [0-9]\+\)$$' integration.coverage.out > integration.coverage-bodged.out - GO111MODULE=on $(GO) run build/gocovmerge.go integration.coverage-bodged.out coverage-bodged.out > coverage.all || (echo "gocovmerge failed"; echo "integration.coverage.out"; cat integration.coverage.out; echo "coverage.out"; cat coverage.out; exit 1) + $(GO) run build/gocovmerge.go integration.coverage-bodged.out coverage-bodged.out > coverage.all || (echo "gocovmerge failed"; echo "integration.coverage.out"; cat integration.coverage.out; echo "coverage.out"; cat coverage.out; exit 1) .PHONY: unit-test-coverage unit-test-coverage: @@ -754,11 +753,11 @@ update-translations: .PHONY: generate-license generate-license: - GO111MODULE=on $(GO) run build/generate-licenses.go + $(GO) run build/generate-licenses.go .PHONY: generate-gitignore generate-gitignore: - GO111MODULE=on $(GO) run build/generate-gitignores.go + $(GO) run build/generate-gitignores.go .PHONY: generate-images generate-images: | node_modules diff --git a/cmd/admin_auth_ldap.go b/cmd/admin_auth_ldap.go index ec86b2c671d47..9040def822e37 100644 --- a/cmd/admin_auth_ldap.go +++ b/cmd/admin_auth_ldap.go @@ -34,6 +34,10 @@ var ( Name: "not-active", Usage: "Deactivate the authentication source.", }, + cli.BoolFlag{ + Name: "active", + Usage: "Activate the authentication source.", + }, cli.StringFlag{ Name: "security-protocol", Usage: "Security protocol name.", @@ -117,6 +121,10 @@ var ( Name: "synchronize-users", Usage: "Enable user synchronization.", }, + cli.BoolFlag{ + Name: "disable-synchronize-users", + Usage: "Disable user synchronization.", + }, cli.UintFlag{ Name: "page-size", Usage: "Search page size.", @@ -183,9 +191,15 @@ func parseAuthSource(c *cli.Context, authSource *auth.Source) { if c.IsSet("not-active") { authSource.IsActive = !c.Bool("not-active") } + if c.IsSet("active") { + authSource.IsActive = c.Bool("active") + } if c.IsSet("synchronize-users") { authSource.IsSyncEnabled = c.Bool("synchronize-users") } + if c.IsSet("disable-synchronize-users") { + authSource.IsSyncEnabled = !c.Bool("disable-synchronize-users") + } } // parseLdapConfig assigns values on config according to command line flags. diff --git a/cmd/admin_auth_ldap_test.go b/cmd/admin_auth_ldap_test.go index f050b536fdf50..2180b24be58df 100644 --- a/cmd/admin_auth_ldap_test.go +++ b/cmd/admin_auth_ldap_test.go @@ -858,6 +858,36 @@ func TestUpdateLdapBindDn(t *testing.T) { }, errMsg: "Invalid authentication type. expected: LDAP (via BindDN), actual: OAuth2", }, + // case 24 + { + args: []string{ + "ldap-test", + "--id", "24", + "--name", "ldap (via Bind DN) flip 'active' and 'user sync' attributes", + "--active", + "--disable-synchronize-users", + }, + id: 24, + existingAuthSource: &auth.Source{ + Type: auth.LDAP, + IsActive: false, + IsSyncEnabled: true, + Cfg: &ldap.Source{ + Name: "ldap (via Bind DN) flip 'active' and 'user sync' attributes", + Enabled: true, + }, + }, + authSource: &auth.Source{ + Type: auth.LDAP, + Name: "ldap (via Bind DN) flip 'active' and 'user sync' attributes", + IsActive: true, + IsSyncEnabled: false, + Cfg: &ldap.Source{ + Name: "ldap (via Bind DN) flip 'active' and 'user sync' attributes", + Enabled: true, + }, + }, + }, } for n, c := range cases { @@ -1221,6 +1251,33 @@ func TestUpdateLdapSimpleAuth(t *testing.T) { }, errMsg: "Invalid authentication type. expected: LDAP (simple auth), actual: PAM", }, + // case 20 + { + args: []string{ + "ldap-test", + "--id", "20", + "--name", "ldap (simple auth) flip 'active' attribute", + "--active", + }, + id: 20, + existingAuthSource: &auth.Source{ + Type: auth.DLDAP, + IsActive: false, + Cfg: &ldap.Source{ + Name: "ldap (simple auth) flip 'active' attribute", + Enabled: true, + }, + }, + authSource: &auth.Source{ + Type: auth.DLDAP, + Name: "ldap (simple auth) flip 'active' attribute", + IsActive: true, + Cfg: &ldap.Source{ + Name: "ldap (simple auth) flip 'active' attribute", + Enabled: true, + }, + }, + }, } for n, c := range cases { diff --git a/custom/conf/app.example.ini b/custom/conf/app.example.ini index 01f5cb6a47926..fb43ea95a1d44 100644 --- a/custom/conf/app.example.ini +++ b/custom/conf/app.example.ini @@ -603,7 +603,10 @@ ROUTER = console ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; ;; The path of git executable. If empty, Gitea searches through the PATH environment. -PATH = +;PATH = +;; +;; The HOME directory for Git +;HOME_PATH = %(APP_DATA_PATH)/home ;; ;; Disables highlight of added and removed changes ;DISABLE_DIFF_HIGHLIGHT = false @@ -1231,7 +1234,7 @@ PATH = ;; Define allowed algorithms and their minimum key length (use -1 to disable a type) ;ED25519 = 256 ;ECDSA = 256 -;RSA = 2048 +;RSA = 2047 ; we allow 2047 here because an otherwise valid 2048 bit RSA key can be reported as having 2047 bit length ;DSA = -1 ; set to 1024 to switch on ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; diff --git a/docs/content/doc/advanced/config-cheat-sheet.en-us.md b/docs/content/doc/advanced/config-cheat-sheet.en-us.md index 1d067d1f1ca9d..84e3c6ae33df3 100644 --- a/docs/content/doc/advanced/config-cheat-sheet.en-us.md +++ b/docs/content/doc/advanced/config-cheat-sheet.en-us.md @@ -621,7 +621,7 @@ Define allowed algorithms and their minimum key length (use -1 to disable a type - `ED25519`: **256** - `ECDSA`: **256** -- `RSA`: **2048** +- `RSA`: **2047**: We set 2047 here because an otherwise valid 2048 RSA key can be reported as 2047 length. - `DSA`: **-1**: DSA is now disabled by default. Set to **1024** to re-enable but ensure you may need to reconfigure your SSHD provider ## Webhook (`webhook`) @@ -948,6 +948,8 @@ Default templates for project boards: ## Git (`git`) - `PATH`: **""**: The path of Git executable. If empty, Gitea searches through the PATH environment. +- `HOME_PATH`: **%(APP_DATA_PATH)/home**: The HOME directory for Git. + This directory will be used to contain the `.gitconfig` and possible `.gnupg` directories that Gitea's git calls will use. If you can confirm Gitea is the only application running in this environment, you can set it to the normal home directory for Gitea user. - `DISABLE_DIFF_HIGHLIGHT`: **false**: Disables highlight of added and removed changes. - `MAX_GIT_DIFF_LINES`: **1000**: Max number of lines allowed of a single file in diff view. - `MAX_GIT_DIFF_LINE_CHARACTERS`: **5000**: Max character count per line highlighted in diff view. diff --git a/docs/content/doc/advanced/signing.en-us.md b/docs/content/doc/advanced/signing.en-us.md index 8ae2f94e9ece9..9ef94deb75ce4 100644 --- a/docs/content/doc/advanced/signing.en-us.md +++ b/docs/content/doc/advanced/signing.en-us.md @@ -97,10 +97,11 @@ repositories, `SIGNING_KEY=default` could be used to provide different signing keys on a per-repository basis. However, this is clearly not an ideal UI and therefore subject to change. -**Since 1.17**, Gitea runs git in its own home directory `[repository].ROOT` and uses its own config `{[repository].ROOT}/.gitconfig`. +**Since 1.17**, Gitea runs git in its own home directory `[git].HOME_PATH` (default to `%(APP_DATA_PATH)/home`) +and uses its own config `{[git].HOME_PATH}/.gitconfig`. If you have your own customized git config for Gitea, you should set these configs in system git config (aka `/etc/gitconfig`) -or the Gitea internal git config `{[repository].ROOT}/.gitconfig`. -Related home files for git command (like `.gnupg`) should also be put in Gitea's git home directory `[repository].ROOT`. +or the Gitea internal git config `{[git].HOME_PATH}/.gitconfig`. +Related home files for git command (like `.gnupg`) should also be put in Gitea's git home directory `[git].HOME_PATH`. ### `INITIAL_COMMIT` diff --git a/integrations/migrate_test.go b/integrations/migrate_test.go index 9b59c85a4eb7e..f67e4ed2297de 100644 --- a/integrations/migrate_test.go +++ b/integrations/migrate_test.go @@ -5,12 +5,17 @@ package integrations import ( + "fmt" + "net/http" + "net/url" "os" "testing" + 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/modules/structs" "code.gitea.io/gitea/services/migrations" "github.com/stretchr/testify/assert" @@ -40,3 +45,54 @@ func TestMigrateLocalPath(t *testing.T) { setting.ImportLocalPaths = old } + +func TestMigrateGiteaForm(t *testing.T) { + onGiteaRun(t, func(t *testing.T, u *url.URL) { + AllowLocalNetworks := setting.Migrations.AllowLocalNetworks + setting.Migrations.AllowLocalNetworks = true + AppVer := setting.AppVer + // Gitea SDK (go-sdk) need to parse the AppVer from server response, so we must set it to a valid version string. + setting.AppVer = "1.16.0" + defer func() { + setting.Migrations.AllowLocalNetworks = AllowLocalNetworks + setting.AppVer = AppVer + migrations.Init() + }() + assert.NoError(t, migrations.Init()) + + ownerName := "user2" + repoName := "repo1" + repoOwner := unittest.AssertExistsAndLoadBean(t, &user_model.User{Name: ownerName}).(*user_model.User) + session := loginUser(t, ownerName) + token := getTokenForLoggedInUser(t, session) + + // Step 0: verify the repo is available + req := NewRequestf(t, "GET", fmt.Sprintf("/%s/%s", ownerName, repoName)) + _ = session.MakeRequest(t, req, http.StatusOK) + // Step 1: get the Gitea migration form + req = NewRequestf(t, "GET", "/repo/migrate/?service_type=%d", structs.GiteaService) + resp := session.MakeRequest(t, req, http.StatusOK) + // Step 2: load the form + htmlDoc := NewHTMLParser(t, resp.Body) + link, exists := htmlDoc.doc.Find(`form.ui.form[action^="/repo/migrate"]`).Attr("action") + assert.True(t, exists, "The template has changed") + // Step 4: submit the migration to only migrate issues + migratedRepoName := "otherrepo" + req = NewRequestWithValues(t, "POST", link, map[string]string{ + "_csrf": htmlDoc.GetCSRF(), + "service": fmt.Sprintf("%d", structs.GiteaService), + "clone_addr": fmt.Sprintf("%s%s/%s", u, ownerName, repoName), + "auth_token": token, + "issues": "on", + "repo_name": migratedRepoName, + "description": "", + "uid": fmt.Sprintf("%d", repoOwner.ID), + }) + resp = session.MakeRequest(t, req, http.StatusSeeOther) + // Step 5: a redirection displays the migrated repository + loc := resp.Header().Get("Location") + assert.EqualValues(t, fmt.Sprintf("/%s/%s", ownerName, migratedRepoName), loc) + // Step 6: check the repo was created + unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{Name: migratedRepoName}) + }) +} diff --git a/models/issues/milestone.go b/models/issues/milestone.go index fba599e6ece48..c49799f391dc3 100644 --- a/models/issues/milestone.go +++ b/models/issues/milestone.go @@ -361,7 +361,7 @@ func (opts GetMilestonesOption) toCond() builder.Cond { } if len(opts.Name) != 0 { - cond = cond.And(builder.Like{"name", opts.Name}) + cond = cond.And(builder.Like{"UPPER(name)", strings.ToUpper(opts.Name)}) } return cond diff --git a/models/unittest/testdb.go b/models/unittest/testdb.go index baea46dbce68f..7f9af553747cb 100644 --- a/models/unittest/testdb.go +++ b/models/unittest/testdb.go @@ -107,6 +107,8 @@ func MainTest(m *testing.M, testOpts *TestOptions) { setting.Packages.Storage.Path = filepath.Join(setting.AppDataPath, "packages") + setting.Git.HomePath = filepath.Join(setting.AppDataPath, "home") + if err = storage.Init(); err != nil { fatalTestError("storage.Init: %v\n", err) } diff --git a/models/user/user.go b/models/user/user.go index 9460bd38fe428..125c643f3ee88 100644 --- a/models/user/user.go +++ b/models/user/user.go @@ -316,37 +316,45 @@ func (u *User) GenerateEmailActivateCode(email string) string { } // GetUserFollowers returns range of user's followers. -func GetUserFollowers(u *User, listOptions db.ListOptions) ([]*User, error) { - sess := db.GetEngine(db.DefaultContext). +func GetUserFollowers(ctx context.Context, u, viewer *User, listOptions db.ListOptions) ([]*User, int64, error) { + sess := db.GetEngine(ctx). + Select("`user`.*"). + Join("LEFT", "follow", "`user`.id=follow.user_id"). Where("follow.follow_id=?", u.ID). - Join("LEFT", "follow", "`user`.id=follow.user_id") + And(isUserVisibleToViewerCond(viewer)) if listOptions.Page != 0 { sess = db.SetSessionPagination(sess, &listOptions) users := make([]*User, 0, listOptions.PageSize) - return users, sess.Find(&users) + count, err := sess.FindAndCount(&users) + return users, count, err } users := make([]*User, 0, 8) - return users, sess.Find(&users) + count, err := sess.FindAndCount(&users) + return users, count, err } // GetUserFollowing returns range of user's following. -func GetUserFollowing(u *User, listOptions db.ListOptions) ([]*User, error) { +func GetUserFollowing(ctx context.Context, u, viewer *User, listOptions db.ListOptions) ([]*User, int64, error) { sess := db.GetEngine(db.DefaultContext). + Select("`user`.*"). + Join("LEFT", "follow", "`user`.id=follow.follow_id"). Where("follow.user_id=?", u.ID). - Join("LEFT", "follow", "`user`.id=follow.follow_id") + And(isUserVisibleToViewerCond(viewer)) if listOptions.Page != 0 { sess = db.SetSessionPagination(sess, &listOptions) users := make([]*User, 0, listOptions.PageSize) - return users, sess.Find(&users) + count, err := sess.FindAndCount(&users) + return users, count, err } users := make([]*User, 0, 8) - return users, sess.Find(&users) + count, err := sess.FindAndCount(&users) + return users, count, err } // NewGitSig generates and returns the signature of given user. @@ -485,6 +493,9 @@ func (u *User) GitName() string { // ShortName ellipses username to length func (u *User) ShortName(length int) string { + if setting.UI.DefaultShowFullName && len(u.FullName) > 0 { + return base.EllipsisString(u.FullName, length) + } return base.EllipsisString(u.Name, length) } @@ -1219,6 +1230,39 @@ func GetAdminUser() (*User, error) { return &admin, nil } +func isUserVisibleToViewerCond(viewer *User) builder.Cond { + if viewer != nil && viewer.IsAdmin { + return builder.NewCond() + } + + if viewer == nil || viewer.IsRestricted { + return builder.Eq{ + "`user`.visibility": structs.VisibleTypePublic, + } + } + + return builder.Neq{ + "`user`.visibility": structs.VisibleTypePrivate, + }.Or( + builder.In("`user`.id", + builder. + Select("`follow`.user_id"). + From("follow"). + Where(builder.Eq{"`follow`.follow_id": viewer.ID})), + builder.In("`user`.id", + builder. + Select("`team_user`.uid"). + From("team_user"). + Join("INNER", "`team_user` AS t2", "`team_user`.id = `t2`.id"). + Where(builder.Eq{"`t2`.uid": viewer.ID})), + builder.In("`user`.id", + builder. + Select("`team_user`.uid"). + From("team_user"). + Join("INNER", "`team_user` AS t2", "`team_user`.org_id = `t2`.org_id"). + Where(builder.Eq{"`t2`.uid": viewer.ID}))) +} + // IsUserVisibleToViewer check if viewer is able to see user profile func IsUserVisibleToViewer(ctx context.Context, u, viewer *User) bool { if viewer != nil && viewer.IsAdmin { diff --git a/modules/git/command.go b/modules/git/command.go index d71497f1d791e..a1bacbb707a25 100644 --- a/modules/git/command.go +++ b/modules/git/command.go @@ -105,23 +105,36 @@ type RunOpts struct { PipelineFunc func(context.Context, context.CancelFunc) error } -// CommonGitCmdEnvs returns the common environment variables for a "git" command. -func CommonGitCmdEnvs() []string { +func commonBaseEnvs() []string { // at the moment, do not set "GIT_CONFIG_NOSYSTEM", users may have put some configs like "receive.certNonceSeed" in it - return []string{ - fmt.Sprintf("LC_ALL=%s", DefaultLocale), - "GIT_TERMINAL_PROMPT=0", // avoid prompting for credentials interactively, supported since git v2.3 - "GIT_NO_REPLACE_OBJECTS=1", // ignore replace references (https://git-scm.com/docs/git-replace) + envs := []string{ "HOME=" + HomeDir(), // make Gitea use internal git config only, to prevent conflicts with user's git config + "GIT_NO_REPLACE_OBJECTS=1", // ignore replace references (https://git-scm.com/docs/git-replace) + } + + // some environment variables should be passed to git command + passThroughEnvKeys := []string{ + "GNUPGHOME", // git may call gnupg to do commit signing + } + for _, key := range passThroughEnvKeys { + if val, ok := os.LookupEnv(key); ok { + envs = append(envs, key+"="+val) + } } + return envs +} + +// CommonGitCmdEnvs returns the common environment variables for a "git" command. +func CommonGitCmdEnvs() []string { + return append(commonBaseEnvs(), []string{ + "LC_ALL=" + DefaultLocale, + "GIT_TERMINAL_PROMPT=0", // avoid prompting for credentials interactively, supported since git v2.3 + }...) } // CommonCmdServEnvs is like CommonGitCmdEnvs but it only returns minimal required environment variables for the "gitea serv" command func CommonCmdServEnvs() []string { - return []string{ - "GIT_NO_REPLACE_OBJECTS=1", // ignore replace references (https://git-scm.com/docs/git-replace) - "HOME=" + HomeDir(), // make Gitea use internal git config only, to prevent conflicts with user's git config - } + return commonBaseEnvs() } // Run runs the command with the RunOpts diff --git a/modules/git/git.go b/modules/git/git.go index 0652a75f9aeff..3bc08ff93b5fa 100644 --- a/modules/git/git.go +++ b/modules/git/git.go @@ -11,6 +11,7 @@ import ( "fmt" "os" "os/exec" + "path/filepath" "regexp" "runtime" "strings" @@ -19,7 +20,6 @@ import ( "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/setting" - "github.com/hashicorp/go-version" ) @@ -126,8 +126,8 @@ func VersionInfo() string { } func checkInit() error { - if setting.RepoRootPath == "" { - return errors.New("can not init Git's HomeDir (RepoRootPath is empty), the setting and git modules are not initialized correctly") + if setting.Git.HomePath == "" { + return errors.New("unable to init Git's HomeDir, incorrect initialization of the setting and git modules") } if DefaultContext != nil { log.Warn("git module has been initialized already, duplicate init should be fixed") @@ -137,14 +137,14 @@ func checkInit() error { // HomeDir is the home dir for git to store the global config file used by Gitea internally func HomeDir() string { - if setting.RepoRootPath == "" { + if setting.Git.HomePath == "" { // strict check, make sure the git module is initialized correctly. // attention: when the git module is called in gitea sub-command (serv/hook), the log module is not able to show messages to users. // for example: if there is gitea git hook code calling git.NewCommand before git.InitXxx, the integration test won't show the real failure reasons. - log.Fatal("can not get Git's HomeDir (RepoRootPath is empty), the setting and git modules are not initialized correctly") + log.Fatal("Unable to init Git's HomeDir, incorrect initialization of the setting and git modules") return "" } - return setting.RepoRootPath + return setting.Git.HomePath } // InitSimple initializes git module with a very simple step, no config changes, no global command arguments. @@ -175,11 +175,15 @@ func InitOnceWithSync(ctx context.Context) (err error) { } initOnce.Do(func() { - err = InitSimple(ctx) - if err != nil { + if err = InitSimple(ctx); err != nil { return } + // when git works with gnupg (commit signing), there should be a stable home for gnupg commands + if _, ok := os.LookupEnv("GNUPGHOME"); !ok { + _ = os.Setenv("GNUPGHOME", filepath.Join(HomeDir(), ".gnupg")) + } + // Since git wire protocol has been released from git v2.18 if setting.Git.EnableAutoGitWireProtocol && CheckGitVersionAtLeast("2.18") == nil { globalCommandArgs = append(globalCommandArgs, "-c", "protocol.version=2") @@ -206,7 +210,7 @@ func InitOnceWithSync(ctx context.Context) (err error) { // syncGitConfig only modifies gitconfig, won't change global variables (otherwise there will be data-race problem) func syncGitConfig() (err error) { if err = os.MkdirAll(HomeDir(), os.ModePerm); err != nil { - return fmt.Errorf("unable to create directory %s, err: %w", setting.RepoRootPath, err) + return fmt.Errorf("unable to prepare git home directory %s, err: %w", HomeDir(), err) } // Git requires setting user.name and user.email in order to commit changes - old comment: "if they're not set just add some defaults" diff --git a/modules/git/git_test.go b/modules/git/git_test.go index c1a9ec351aaa5..c5a63de0644c0 100644 --- a/modules/git/git_test.go +++ b/modules/git/git_test.go @@ -21,12 +21,12 @@ import ( func testRun(m *testing.M) error { _ = log.NewLogger(1000, "console", "console", `{"level":"trace","stacktracelevel":"NONE","stderr":true}`) - repoRootPath, err := os.MkdirTemp(os.TempDir(), "repos") + gitHomePath, err := os.MkdirTemp(os.TempDir(), "git-home") if err != nil { return fmt.Errorf("unable to create temp dir: %w", err) } - defer util.RemoveAll(repoRootPath) - setting.RepoRootPath = repoRootPath + defer util.RemoveAll(gitHomePath) + setting.Git.HomePath = gitHomePath if err = InitOnceWithSync(context.Background()); err != nil { return fmt.Errorf("failed to call Init: %w", err) diff --git a/modules/markup/html.go b/modules/markup/html.go index 69d9ba3ef2a46..6071180501c42 100644 --- a/modules/markup/html.go +++ b/modules/markup/html.go @@ -841,9 +841,10 @@ func issueIndexPatternProcessor(ctx *RenderContext, node *html.Node) { // Repos with external issue trackers might still need to reference local PRs // We need to concern with the first one that shows up in the text, whichever it is - if hasExtTrackFormat && !isNumericStyle { + if hasExtTrackFormat && !isNumericStyle && refNumeric != nil { // If numeric (PR) was found, and it was BEFORE the non-numeric pattern, use that - if foundNumeric && refNumeric.RefLocation.Start < ref.RefLocation.Start { + // Allow a free-pass when non-numeric pattern wasn't found. + if found && (ref == nil || refNumeric.RefLocation.Start < ref.RefLocation.Start) { found = foundNumeric ref = refNumeric } diff --git a/modules/setting/git.go b/modules/setting/git.go index 9b2698f01e780..266bbc3c5aa3a 100644 --- a/modules/setting/git.go +++ b/modules/setting/git.go @@ -5,6 +5,7 @@ package setting import ( + "path/filepath" "time" "code.gitea.io/gitea/modules/log" @@ -13,6 +14,7 @@ import ( // Git settings var Git = struct { Path string + HomePath string DisableDiffHighlight bool MaxGitDiffLines int MaxGitDiffLineCharacters int @@ -67,7 +69,16 @@ var Git = struct { } func newGit() { - if err := Cfg.Section("git").MapTo(&Git); err != nil { + sec := Cfg.Section("git") + + if err := sec.MapTo(&Git); err != nil { log.Fatal("Failed to map Git settings: %v", err) } + + Git.HomePath = sec.Key("HOME_PATH").MustString("home") + if !filepath.IsAbs(Git.HomePath) { + Git.HomePath = filepath.Join(AppDataPath, Git.HomePath) + } else { + Git.HomePath = filepath.Clean(Git.HomePath) + } } diff --git a/modules/setting/setting.go b/modules/setting/setting.go index 5400bf5b9f95c..510e00e5ab120 100644 --- a/modules/setting/setting.go +++ b/modules/setting/setting.go @@ -170,7 +170,7 @@ var ( ServerMACs: []string{"hmac-sha2-256-etm@openssh.com", "hmac-sha2-256", "hmac-sha1"}, KeygenPath: "ssh-keygen", MinimumKeySizeCheck: true, - MinimumKeySizes: map[string]int{"ed25519": 256, "ed25519-sk": 256, "ecdsa": 256, "ecdsa-sk": 256, "rsa": 2048}, + MinimumKeySizes: map[string]int{"ed25519": 256, "ed25519-sk": 256, "ecdsa": 256, "ecdsa-sk": 256, "rsa": 2047}, ServerHostKeys: []string{"ssh/gitea.rsa", "ssh/gogs.rsa"}, AuthorizedKeysCommandTemplate: "{{.AppPath}} --config={{.CustomConf}} serv key-{{.Key.ID}}", PerWriteTimeout: PerWriteTimeout, diff --git a/modules/translation/i18n/i18n.go b/modules/translation/i18n/i18n.go index acce5f19fb0dc..bb906f3c08c1d 100644 --- a/modules/translation/i18n/i18n.go +++ b/modules/translation/i18n/i18n.go @@ -25,9 +25,13 @@ var ( ) type locale struct { + // This mutex will be set if we have live-reload enabled (e.g. dev mode) + reloadMu *sync.RWMutex + store *LocaleStore langName string - textMap map[int]string // the map key (idx) is generated by store's textIdxMap + + idxToMsgMap map[int]string // the map idx is generated by store's trKeyToIdxMap sourceFileName string sourceFileInfo os.FileInfo @@ -35,164 +39,254 @@ type locale struct { } type LocaleStore struct { - reloadMu *sync.Mutex // for non-prod(dev), use a mutex for live-reload. for prod, no mutex, no live-reload. + // This mutex will be set if we have live-reload enabled (e.g. dev mode) + reloadMu *sync.RWMutex langNames []string langDescs []string + localeMap map[string]*locale - localeMap map[string]*locale - textIdxMap map[string]int + // this needs to be locked when live-reloading + trKeyToIdxMap map[string]int defaultLang string } func NewLocaleStore(isProd bool) *LocaleStore { - ls := &LocaleStore{localeMap: make(map[string]*locale), textIdxMap: make(map[string]int)} + store := &LocaleStore{localeMap: make(map[string]*locale), trKeyToIdxMap: make(map[string]int)} if !isProd { - ls.reloadMu = &sync.Mutex{} + store.reloadMu = &sync.RWMutex{} } - return ls + return store } // AddLocaleByIni adds locale by ini into the store -// if source is a string, then the file is loaded. in dev mode, the file can be live-reloaded +// if source is a string, then the file is loaded. In dev mode, this file will be checked for live-reloading // if source is a []byte, then the content is used -func (ls *LocaleStore) AddLocaleByIni(langName, langDesc string, source interface{}) error { - if _, ok := ls.localeMap[langName]; ok { +// Note: this is not concurrent safe +func (store *LocaleStore) AddLocaleByIni(langName, langDesc string, source interface{}) error { + if _, ok := store.localeMap[langName]; ok { return ErrLocaleAlreadyExist } - lc := &locale{store: ls, langName: langName} + l := &locale{store: store, langName: langName} + if store.reloadMu != nil { + l.reloadMu = &sync.RWMutex{} + l.reloadMu.Lock() // Arguably this is not necessary as AddLocaleByIni isn't concurrent safe - but for consistency we do this + defer l.reloadMu.Unlock() + } + if fileName, ok := source.(string); ok { - lc.sourceFileName = fileName - lc.sourceFileInfo, _ = os.Stat(fileName) // live-reload only works for regular files. the error can be ignored + l.sourceFileName = fileName + l.sourceFileInfo, _ = os.Stat(fileName) // live-reload only works for regular files. the error can be ignored + } + + var err error + l.idxToMsgMap, err = store.readIniToIdxToMsgMap(source) + if err != nil { + return err } - ls.langNames = append(ls.langNames, langName) - ls.langDescs = append(ls.langDescs, langDesc) - ls.localeMap[lc.langName] = lc + store.langNames = append(store.langNames, langName) + store.langDescs = append(store.langDescs, langDesc) + + store.localeMap[l.langName] = l - return ls.reloadLocaleByIni(langName, source) + return nil } -func (ls *LocaleStore) reloadLocaleByIni(langName string, source interface{}) error { +// readIniToIdxToMsgMap will read a provided ini and creates an idxToMsgMap +func (store *LocaleStore) readIniToIdxToMsgMap(source interface{}) (map[int]string, error) { iniFile, err := ini.LoadSources(ini.LoadOptions{ IgnoreInlineComment: true, UnescapeValueCommentSymbols: true, }, source) if err != nil { - return fmt.Errorf("unable to load ini: %w", err) + return nil, fmt.Errorf("unable to load ini: %w", err) } iniFile.BlockMode = false - lc := ls.localeMap[langName] - lc.textMap = make(map[int]string) + idxToMsgMap := make(map[int]string) + + if store.reloadMu != nil { + store.reloadMu.Lock() + defer store.reloadMu.Unlock() + } + for _, section := range iniFile.Sections() { for _, key := range section.Keys() { + var trKey string if section.Name() == "" || section.Name() == "DEFAULT" { trKey = key.Name() } else { trKey = section.Name() + "." + key.Name() } - textIdx, ok := ls.textIdxMap[trKey] + + // Instead of storing the key strings in multiple different maps we compute a idx which will act as numeric code for key + // This reduces the size of the locale idxToMsgMaps + idx, ok := store.trKeyToIdxMap[trKey] if !ok { - textIdx = len(ls.textIdxMap) - ls.textIdxMap[trKey] = textIdx + idx = len(store.trKeyToIdxMap) + store.trKeyToIdxMap[trKey] = idx } - lc.textMap[textIdx] = key.Value() + idxToMsgMap[idx] = key.Value() } } iniFile = nil - return nil + return idxToMsgMap, nil +} + +func (store *LocaleStore) idxForTrKey(trKey string) (int, bool) { + if store.reloadMu != nil { + store.reloadMu.RLock() + defer store.reloadMu.RUnlock() + } + idx, ok := store.trKeyToIdxMap[trKey] + return idx, ok } -func (ls *LocaleStore) HasLang(langName string) bool { - _, ok := ls.localeMap[langName] +// HasLang reports if a language is available in the store +func (store *LocaleStore) HasLang(langName string) bool { + _, ok := store.localeMap[langName] return ok } -func (ls *LocaleStore) ListLangNameDesc() (names, desc []string) { - return ls.langNames, ls.langDescs +// ListLangNameDesc reports if a language available in the store +func (store *LocaleStore) ListLangNameDesc() (names, desc []string) { + return store.langNames, store.langDescs } // SetDefaultLang sets default language as a fallback -func (ls *LocaleStore) SetDefaultLang(lang string) { - ls.defaultLang = lang +func (store *LocaleStore) SetDefaultLang(lang string) { + store.defaultLang = lang } // Tr translates content to target language. fall back to default language. -func (ls *LocaleStore) Tr(lang, trKey string, trArgs ...interface{}) string { - l, ok := ls.localeMap[lang] +func (store *LocaleStore) Tr(lang, trKey string, trArgs ...interface{}) string { + l, ok := store.localeMap[lang] if !ok { - l, ok = ls.localeMap[ls.defaultLang] + l, ok = store.localeMap[store.defaultLang] } + if ok { return l.Tr(trKey, trArgs...) } return trKey } +// reloadIfNeeded will check if the locale needs to be reloaded +// this function will assume that the l.reloadMu has been RLocked if it already exists +func (l *locale) reloadIfNeeded() { + if l.reloadMu == nil { + return + } + + now := time.Now() + if now.Sub(l.lastReloadCheckTime) < time.Second || l.sourceFileInfo == nil || l.sourceFileName == "" { + return + } + + l.reloadMu.RUnlock() + l.reloadMu.Lock() // (NOTE: a pre-emption can occur between these two locks so we need to recheck) + defer l.reloadMu.RLock() + defer l.reloadMu.Unlock() + + if now.Sub(l.lastReloadCheckTime) < time.Second || l.sourceFileInfo == nil || l.sourceFileName == "" { + return + } + + l.lastReloadCheckTime = now + sourceFileInfo, err := os.Stat(l.sourceFileName) + if err != nil || sourceFileInfo.ModTime().Equal(l.sourceFileInfo.ModTime()) { + return + } + + idxToMsgMap, err := l.store.readIniToIdxToMsgMap(l.sourceFileName) + if err == nil { + l.idxToMsgMap = idxToMsgMap + } else { + log.Error("Unable to live-reload the locale file %q, err: %v", l.sourceFileName, err) + } + + // We will set the sourceFileInfo to this file to prevent repeated attempts to re-load this broken file + l.sourceFileInfo = sourceFileInfo +} + // Tr translates content to locale language. fall back to default language. func (l *locale) Tr(trKey string, trArgs ...interface{}) string { - if l.store.reloadMu != nil { - l.store.reloadMu.Lock() - defer l.store.reloadMu.Unlock() - now := time.Now() - if now.Sub(l.lastReloadCheckTime) >= time.Second && l.sourceFileInfo != nil && l.sourceFileName != "" { - l.lastReloadCheckTime = now - if sourceFileInfo, err := os.Stat(l.sourceFileName); err == nil && !sourceFileInfo.ModTime().Equal(l.sourceFileInfo.ModTime()) { - if err = l.store.reloadLocaleByIni(l.langName, l.sourceFileName); err == nil { - l.sourceFileInfo = sourceFileInfo - } else { - log.Error("unable to live-reload the locale file %q, err: %v", l.sourceFileName, err) - } - } - } + if l.reloadMu != nil { + l.reloadMu.RLock() + defer l.reloadMu.RUnlock() + l.reloadIfNeeded() } + msg, _ := l.tryTr(trKey, trArgs...) return msg } func (l *locale) tryTr(trKey string, trArgs ...interface{}) (msg string, found bool) { trMsg := trKey - textIdx, ok := l.store.textIdxMap[trKey] + + // convert the provided trKey to a common idx from the store + idx, ok := l.store.idxForTrKey(trKey) + if ok { - if msg, found = l.textMap[textIdx]; found { - trMsg = msg // use current translation + if msg, found = l.idxToMsgMap[idx]; found { + trMsg = msg // use the translation that we have found } else if l.langName != l.store.defaultLang { + // No translation available in our current language... fallback to the default language + + // Attempt to get the default language from the locale store if def, ok := l.store.localeMap[l.store.defaultLang]; ok { - return def.tryTr(trKey, trArgs...) + + if def.reloadMu != nil { + def.reloadMu.RLock() + def.reloadIfNeeded() + } + if msg, found = def.idxToMsgMap[idx]; found { + trMsg = msg // use the translation that we have found + } + if def.reloadMu != nil { + def.reloadMu.RUnlock() + } } - } else if !setting.IsProd { - log.Error("missing i18n translation key: %q", trKey) } } - if len(trArgs) > 0 { - fmtArgs := make([]interface{}, 0, len(trArgs)) - for _, arg := range trArgs { - val := reflect.ValueOf(arg) - if val.Kind() == reflect.Slice { - // before, it can accept Tr(lang, key, a, [b, c], d, [e, f]) as Sprintf(msg, a, b, c, d, e, f), it's an unstable behavior - // now, we restrict the strange behavior and only support: - // 1. Tr(lang, key, [slice-items]) as Sprintf(msg, items...) - // 2. Tr(lang, key, args...) as Sprintf(msg, args...) - if len(trArgs) == 1 { - for i := 0; i < val.Len(); i++ { - fmtArgs = append(fmtArgs, val.Index(i).Interface()) - } - } else { - log.Error("the args for i18n shouldn't contain uncertain slices, key=%q, args=%v", trKey, trArgs) - break + if !found && !setting.IsProd { + log.Error("missing i18n translation key: %q", trKey) + } + + if len(trArgs) == 0 { + return trMsg, found + } + + fmtArgs := make([]interface{}, 0, len(trArgs)) + for _, arg := range trArgs { + val := reflect.ValueOf(arg) + if val.Kind() == reflect.Slice { + // Previously, we would accept Tr(lang, key, a, [b, c], d, [e, f]) as Sprintf(msg, a, b, c, d, e, f) + // but this is an unstable behavior. + // + // So we restrict the accepted arguments to either: + // + // 1. Tr(lang, key, [slice-items]) as Sprintf(msg, items...) + // 2. Tr(lang, key, args...) as Sprintf(msg, args...) + if len(trArgs) == 1 { + for i := 0; i < val.Len(); i++ { + fmtArgs = append(fmtArgs, val.Index(i).Interface()) } } else { - fmtArgs = append(fmtArgs, arg) + log.Error("the args for i18n shouldn't contain uncertain slices, key=%q, args=%v", trKey, trArgs) + break } + } else { + fmtArgs = append(fmtArgs, arg) } - return fmt.Sprintf(trMsg, fmtArgs...), found } - return trMsg, found + + return fmt.Sprintf(trMsg, fmtArgs...), found } // ResetDefaultLocales resets the current default locales diff --git a/package-lock.json b/package-lock.json index 8f8f7013a51c2..5264acac2589e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -22,7 +22,7 @@ "less": "4.1.2", "less-loader": "11.0.0", "license-checker-webpack-plugin": "0.2.1", - "mermaid": "9.1.1", + "mermaid": "9.1.2", "mini-css-extract-plugin": "2.6.0", "monaco-editor": "0.33.0", "monaco-editor-webpack-plugin": "7.0.1", @@ -3833,9 +3833,9 @@ } }, "node_modules/dompurify": { - "version": "2.3.6", - "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-2.3.6.tgz", - "integrity": "sha512-OFP2u/3T1R5CEgWCEONuJ1a5+MFKnOYpkywpUSxv/dj1LeBT1erK+JwM7zK0ROy2BRhqVCf0LRw/kHqKuMkVGg==" + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-2.3.8.tgz", + "integrity": "sha512-eVhaWoVibIzqdGYjwsBWodIQIaXFSB+cKDf4cfxLMsK0xiud6SE+/WCVx/Xw/UwQsa4cS3T2eITcdtmTg2UKcw==" }, "node_modules/domutils": { "version": "2.8.0", @@ -7741,15 +7741,15 @@ } }, "node_modules/mermaid": { - "version": "9.1.1", - "resolved": "https://registry.npmjs.org/mermaid/-/mermaid-9.1.1.tgz", - "integrity": "sha512-2RVD+WkzZ4VDyO9gQvQAuQ/ux2gLigJtKDTlbwjYqOR/NwsVzTSfGm/kx648/qWJsg6Sv04tE9BWCO8s6a+pFA==", + "version": "9.1.2", + "resolved": "https://registry.npmjs.org/mermaid/-/mermaid-9.1.2.tgz", + "integrity": "sha512-RVf3hBKqiMfyORHboCaEjOAK1TomLO50hYRPvlTrZCXlCniM5pRpe8UlkHBjjpaLtioZnbdYv/vEVj7iKnwkJQ==", "dependencies": { "@braintree/sanitize-url": "^6.0.0", "d3": "^7.0.0", "dagre": "^0.8.5", "dagre-d3": "^0.6.4", - "dompurify": "2.3.6", + "dompurify": "2.3.8", "graphlib": "^2.1.8", "khroma": "^2.0.0", "moment-mini": "^2.24.0", @@ -13815,9 +13815,9 @@ } }, "dompurify": { - "version": "2.3.6", - "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-2.3.6.tgz", - "integrity": "sha512-OFP2u/3T1R5CEgWCEONuJ1a5+MFKnOYpkywpUSxv/dj1LeBT1erK+JwM7zK0ROy2BRhqVCf0LRw/kHqKuMkVGg==" + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-2.3.8.tgz", + "integrity": "sha512-eVhaWoVibIzqdGYjwsBWodIQIaXFSB+cKDf4cfxLMsK0xiud6SE+/WCVx/Xw/UwQsa4cS3T2eITcdtmTg2UKcw==" }, "domutils": { "version": "2.8.0", @@ -16860,15 +16860,15 @@ "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==" }, "mermaid": { - "version": "9.1.1", - "resolved": "https://registry.npmjs.org/mermaid/-/mermaid-9.1.1.tgz", - "integrity": "sha512-2RVD+WkzZ4VDyO9gQvQAuQ/ux2gLigJtKDTlbwjYqOR/NwsVzTSfGm/kx648/qWJsg6Sv04tE9BWCO8s6a+pFA==", + "version": "9.1.2", + "resolved": "https://registry.npmjs.org/mermaid/-/mermaid-9.1.2.tgz", + "integrity": "sha512-RVf3hBKqiMfyORHboCaEjOAK1TomLO50hYRPvlTrZCXlCniM5pRpe8UlkHBjjpaLtioZnbdYv/vEVj7iKnwkJQ==", "requires": { "@braintree/sanitize-url": "^6.0.0", "d3": "^7.0.0", "dagre": "^0.8.5", "dagre-d3": "^0.6.4", - "dompurify": "2.3.6", + "dompurify": "2.3.8", "graphlib": "^2.1.8", "khroma": "^2.0.0", "moment-mini": "^2.24.0", diff --git a/package.json b/package.json index addf5633fbf98..2e1c05bb89ef3 100644 --- a/package.json +++ b/package.json @@ -22,7 +22,7 @@ "less": "4.1.2", "less-loader": "11.0.0", "license-checker-webpack-plugin": "0.2.1", - "mermaid": "9.1.1", + "mermaid": "9.1.2", "mini-css-extract-plugin": "2.6.0", "monaco-editor": "0.33.0", "monaco-editor-webpack-plugin": "7.0.1", diff --git a/routers/api/v1/user/follower.go b/routers/api/v1/user/follower.go index 3c81b27f8dad4..22f8f40e1c810 100644 --- a/routers/api/v1/user/follower.go +++ b/routers/api/v1/user/follower.go @@ -24,13 +24,13 @@ func responseAPIUsers(ctx *context.APIContext, users []*user_model.User) { } func listUserFollowers(ctx *context.APIContext, u *user_model.User) { - users, err := user_model.GetUserFollowers(u, utils.GetListOptions(ctx)) + users, count, err := user_model.GetUserFollowers(ctx, u, ctx.Doer, utils.GetListOptions(ctx)) if err != nil { ctx.Error(http.StatusInternalServerError, "GetUserFollowers", err) return } - ctx.SetTotalCountHeader(int64(u.NumFollowers)) + ctx.SetTotalCountHeader(count) responseAPIUsers(ctx, users) } @@ -86,13 +86,13 @@ func ListFollowers(ctx *context.APIContext) { } func listUserFollowing(ctx *context.APIContext, u *user_model.User) { - users, err := user_model.GetUserFollowing(u, utils.GetListOptions(ctx)) + users, count, err := user_model.GetUserFollowing(ctx, u, ctx.Doer, utils.GetListOptions(ctx)) if err != nil { ctx.Error(http.StatusInternalServerError, "GetUserFollowing", err) return } - ctx.SetTotalCountHeader(int64(u.NumFollowing)) + ctx.SetTotalCountHeader(count) responseAPIUsers(ctx, users) } diff --git a/routers/web/user/profile.go b/routers/web/user/profile.go index 609f3242c65e2..6f23d239e26a5 100644 --- a/routers/web/user/profile.go +++ b/routers/web/user/profile.go @@ -157,7 +157,7 @@ func Profile(ctx *context.Context) { switch tab { case "followers": - items, err := user_model.GetUserFollowers(ctx.ContextUser, db.ListOptions{ + items, count, err := user_model.GetUserFollowers(ctx, ctx.ContextUser, ctx.Doer, db.ListOptions{ PageSize: setting.UI.User.RepoPagingNum, Page: page, }) @@ -167,9 +167,9 @@ func Profile(ctx *context.Context) { } ctx.Data["Cards"] = items - total = ctx.ContextUser.NumFollowers + total = int(count) case "following": - items, err := user_model.GetUserFollowing(ctx.ContextUser, db.ListOptions{ + items, count, err := user_model.GetUserFollowing(ctx, ctx.ContextUser, ctx.Doer, db.ListOptions{ PageSize: setting.UI.User.RepoPagingNum, Page: page, }) @@ -179,7 +179,7 @@ func Profile(ctx *context.Context) { } ctx.Data["Cards"] = items - total = ctx.ContextUser.NumFollowing + total = int(count) case "activity": ctx.Data["Feeds"], err = models.GetFeeds(ctx, models.GetFeedsOptions{ RequestedUser: ctx.ContextUser, diff --git a/services/auth/source/ldap/source_search.go b/services/auth/source/ldap/source_search.go index 988d56005e011..a97a1179d9bcd 100644 --- a/services/auth/source/ldap/source_search.go +++ b/services/auth/source/ldap/source_search.go @@ -199,7 +199,7 @@ func checkRestricted(l *ldap.Conn, ls *Source, userDN string) bool { // List all group memberships of a user func (source *Source) listLdapGroupMemberships(l *ldap.Conn, uid string) []string { var ldapGroups []string - groupFilter := fmt.Sprintf("(%s=%s)", source.GroupMemberUID, uid) + groupFilter := fmt.Sprintf("(%s=%s)", source.GroupMemberUID, ldap.EscapeFilter(uid)) result, err := l.Search(ldap.NewSearchRequest( source.GroupDN, ldap.ScopeWholeSubtree, diff --git a/templates/base/head_navbar.tmpl b/templates/base/head_navbar.tmpl index b04d8bcbb3850..b9e9ee7cf84fb 100644 --- a/templates/base/head_navbar.tmpl +++ b/templates/base/head_navbar.tmpl @@ -1,8 +1,22 @@
- + {{svg "octicon-bell"}} - {{.locale.Tr "notifications"}} - {{$notificationUnreadCount := 0}} - {{if .NotificationUnreadCount}}{{$notificationUnreadCount = call .NotificationUnreadCount}}{{end}} {{$notificationUnreadCount}} diff --git a/templates/explore/repo_list.tmpl b/templates/explore/repo_list.tmpl index 10b503b219d94..cd75f48520cbe 100644 --- a/templates/explore/repo_list.tmpl +++ b/templates/explore/repo_list.tmpl @@ -32,9 +32,9 @@ {{end}} {{end}} {{if .IsFork}} - {{svg "octicon-repo-forked"}} + {{svg "octicon-repo-forked"}} {{else if .IsMirror}} - {{svg "octicon-mirror"}} + {{svg "octicon-mirror"}} {{end}} diff --git a/web_src/js/bootstrap.js b/web_src/js/bootstrap.js index cf13b2a559d30..213c9e41df5db 100644 --- a/web_src/js/bootstrap.js +++ b/web_src/js/bootstrap.js @@ -20,6 +20,11 @@ export function showGlobalErrorMessage(msg) { * @param {ErrorEvent} e */ function processWindowErrorEvent(e) { + if (!e.error && e.lineno === 0 && e.colno === 0 && e.filename === '' && window.navigator.userAgent.includes('FxiOS/')) { + // At the moment, Firefox (iOS) (10x) has an engine bug. See https://github.com/go-gitea/gitea/issues/20240 + // If a script inserts a newly created (and content changed) element into DOM, there will be a nonsense error event reporting: Script error: line 0, col 0. + return; // ignore such nonsense error event + } showGlobalErrorMessage(`JavaScript error: ${e.message} (${e.filename} @ ${e.lineno}:${e.colno}). Open browser console to see more details.`); } diff --git a/web_src/js/features/common-global.js b/web_src/js/features/common-global.js index a508db39c5e6a..419c5996dc651 100644 --- a/web_src/js/features/common-global.js +++ b/web_src/js/features/common-global.js @@ -75,6 +75,20 @@ export function initGlobalButtonClickOnEnter() { }); } +export function initPopup(target) { + const $el = $(target); + const attr = $el.attr('data-variation'); + const attrs = attr ? attr.split(' ') : []; + const variations = new Set([...attrs, 'inverted', 'tiny']); + $el.attr('data-variation', [...variations].join(' ')).popup(); +} + +export function initGlobalPopups() { + $('.tooltip').each((_, el) => { + initPopup(el); + }); +} + export function initGlobalCommon() { // Show exact time $('.time-since').each(function () { @@ -121,15 +135,6 @@ export function initGlobalCommon() { $('.ui.checkbox').checkbox(); - // init popups - $('.tooltip').each((_, el) => { - const $el = $(el); - const attr = $el.attr('data-variation'); - const attrs = attr ? attr.split(' ') : []; - const variations = new Set([...attrs, 'inverted', 'tiny']); - $el.attr('data-variation', [...variations].join(' ')).popup(); - }); - $('.top.menu .tooltip').popup({ onShow() { if ($('.top.menu .menu.transition').hasClass('visible')) { diff --git a/web_src/js/features/repo-diff.js b/web_src/js/features/repo-diff.js index 8403a2fd42d13..92d8ecfc86096 100644 --- a/web_src/js/features/repo-diff.js +++ b/web_src/js/features/repo-diff.js @@ -3,6 +3,7 @@ import {initCompReactionSelector} from './comp/ReactionSelector.js'; import {initRepoIssueContentHistory} from './repo-issue-content.js'; import {validateTextareaNonEmpty} from './comp/EasyMDE.js'; import {initViewedCheckboxListenerFor, countAndUpdateViewedFiles} from './pull-view-file.js'; +import {initPopup} from './common-global.js'; const {csrfToken} = window.config; @@ -52,6 +53,7 @@ export function initRepoDiffConversationForm() { const newConversationHolder = $(await $.post(form.attr('action'), form.serialize())); const {path, side, idx} = newConversationHolder.data(); + initPopup(newConversationHolder.find('.tooltip')); form.closest('.conversation-holder').replaceWith(newConversationHolder); if (form.closest('tr').data('line-type') === 'same') { $(`[data-path="${path}"] a.add-code-comment[data-idx="${idx}"]`).addClass('invisible'); diff --git a/web_src/js/index.js b/web_src/js/index.js index 0568da64aec51..6f872b5353378 100644 --- a/web_src/js/index.js +++ b/web_src/js/index.js @@ -56,6 +56,7 @@ import { initGlobalFormDirtyLeaveConfirm, initGlobalLinkActions, initHeadNavbarContentToggle, + initGlobalPopups, } from './features/common-global.js'; import {initRepoTopicBar} from './features/repo-home.js'; import {initAdminEmails} from './features/admin-emails.js'; @@ -99,6 +100,7 @@ initVueEnv(); $(document).ready(() => { initGlobalCommon(); + initGlobalPopups(); initGlobalButtonClickOnEnter(); initGlobalButtons(); initGlobalCopyToClipboardListener(); diff --git a/web_src/less/_base.less b/web_src/less/_base.less index 78f32956efce1..638801e3928c4 100644 --- a/web_src/less/_base.less +++ b/web_src/less/_base.less @@ -1329,7 +1329,7 @@ footer { @media @mediaMdAndUp { .mobile-only, .ui.button.mobile-only { - display: none; + display: none !important; } // has the same behaviour of sr-only, hiding the content for @@ -1341,7 +1341,7 @@ footer { @media @mediaSm { .not-mobile { - display: none; + display: none !important; } } diff --git a/web_src/less/_repository.less b/web_src/less/_repository.less index c9ceece530bbc..0ad937f028cab 100644 --- a/web_src/less/_repository.less +++ b/web_src/less/_repository.less @@ -352,11 +352,31 @@ overflow: initial; &.name { - max-width: 150px; + @media @mediaXl { + max-width: 150px; + } + @media @mediaLg { + max-width: 200px; + } + @media @mediaMd { + max-width: 300px; + } + width: 33%; + + max-width: calc(100vw - 140px); } &.message { - max-width: 400px; + @media @mediaXl { + max-width: 400px; + } + @media @mediaLg { + max-width: 350px; + } + @media @mediaMd { + max-width: 250px; + } + width: 66%; } &.age {