Merge remote-tracking branch 'forgejo/forgejo-dependency' into forgejo
This commit is contained in:
commit
188573d069
60 changed files with 780 additions and 43 deletions
|
@ -808,6 +808,11 @@ LEVEL = Info
|
||||||
;; Every new user will have restricted permissions depending on this setting
|
;; Every new user will have restricted permissions depending on this setting
|
||||||
;DEFAULT_USER_IS_RESTRICTED = false
|
;DEFAULT_USER_IS_RESTRICTED = false
|
||||||
;;
|
;;
|
||||||
|
;; Users will be able to use dots when choosing their username. Disabling this is
|
||||||
|
;; helpful if your usersare having issues with e.g. RSS feeds or advanced third-party
|
||||||
|
;; extensions that use strange regex patterns.
|
||||||
|
; ALLOW_DOTS_IN_USERNAMES = true
|
||||||
|
;;
|
||||||
;; Either "public", "limited" or "private", default is "public"
|
;; Either "public", "limited" or "private", default is "public"
|
||||||
;; Limited is for users visible only to signed users
|
;; Limited is for users visible only to signed users
|
||||||
;; Private is for users visible only to members of their organizations
|
;; Private is for users visible only to members of their organizations
|
||||||
|
@ -1450,6 +1455,8 @@ LEVEL = Info
|
||||||
;;
|
;;
|
||||||
;; Default configuration for email notifications for users (user configurable). Options: enabled, onmention, disabled
|
;; Default configuration for email notifications for users (user configurable). Options: enabled, onmention, disabled
|
||||||
;DEFAULT_EMAIL_NOTIFICATIONS = enabled
|
;DEFAULT_EMAIL_NOTIFICATIONS = enabled
|
||||||
|
;; Send email notifications to all instance admins on new user sign-ups. Options: enabled, true, false
|
||||||
|
;NOTIFY_NEW_SIGN_UPS = false
|
||||||
|
|
||||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||||
|
@ -1764,9 +1771,6 @@ LEVEL = Info
|
||||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||||
;;
|
;;
|
||||||
;AVATAR_UPLOAD_PATH = data/avatars
|
|
||||||
;REPOSITORY_AVATAR_UPLOAD_PATH = data/repo-avatars
|
|
||||||
;;
|
|
||||||
;; How Gitea deals with missing repository avatars
|
;; How Gitea deals with missing repository avatars
|
||||||
;; none = no avatar will be displayed; random = random avatar will be displayed; image = default image will be used
|
;; none = no avatar will be displayed; random = random avatar will be displayed; image = default image will be used
|
||||||
;REPOSITORY_AVATAR_FALLBACK = none
|
;REPOSITORY_AVATAR_FALLBACK = none
|
||||||
|
|
|
@ -250,7 +250,7 @@ func (s AccessTokenScope) parse() (accessTokenScopeBitmap, error) {
|
||||||
remainingScopes = remainingScopes[i+1:]
|
remainingScopes = remainingScopes[i+1:]
|
||||||
}
|
}
|
||||||
singleScope := AccessTokenScope(v)
|
singleScope := AccessTokenScope(v)
|
||||||
if singleScope == "" {
|
if singleScope == "" || singleScope == "sudo" {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if singleScope == AccessTokenScopeAll {
|
if singleScope == AccessTokenScopeAll {
|
||||||
|
|
|
@ -20,7 +20,7 @@ func TestAccessTokenScope_Normalize(t *testing.T) {
|
||||||
tests := []scopeTestNormalize{
|
tests := []scopeTestNormalize{
|
||||||
{"", "", nil},
|
{"", "", nil},
|
||||||
{"write:misc,write:notification,read:package,write:notification,public-only", "public-only,write:misc,write:notification,read:package", nil},
|
{"write:misc,write:notification,read:package,write:notification,public-only", "public-only,write:misc,write:notification,read:package", nil},
|
||||||
{"all", "all", nil},
|
{"all,sudo", "all", nil},
|
||||||
{"write:activitypub,write:admin,write:misc,write:notification,write:organization,write:package,write:issue,write:repository,write:user", "all", nil},
|
{"write:activitypub,write:admin,write:misc,write:notification,write:organization,write:package,write:issue,write:repository,write:user", "all", nil},
|
||||||
{"write:activitypub,write:admin,write:misc,write:notification,write:organization,write:package,write:issue,write:repository,write:user,public-only", "public-only,all", nil},
|
{"write:activitypub,write:admin,write:misc,write:notification,write:organization,write:package,write:issue,write:repository,write:user,public-only", "public-only,all", nil},
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,10 +11,13 @@ import (
|
||||||
"io"
|
"io"
|
||||||
"reflect"
|
"reflect"
|
||||||
"strings"
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"code.gitea.io/gitea/modules/log"
|
||||||
"code.gitea.io/gitea/modules/setting"
|
"code.gitea.io/gitea/modules/setting"
|
||||||
|
|
||||||
"xorm.io/xorm"
|
"xorm.io/xorm"
|
||||||
|
"xorm.io/xorm/contexts"
|
||||||
"xorm.io/xorm/names"
|
"xorm.io/xorm/names"
|
||||||
"xorm.io/xorm/schemas"
|
"xorm.io/xorm/schemas"
|
||||||
|
|
||||||
|
@ -147,6 +150,13 @@ func InitEngine(ctx context.Context) error {
|
||||||
xormEngine.SetConnMaxLifetime(setting.Database.ConnMaxLifetime)
|
xormEngine.SetConnMaxLifetime(setting.Database.ConnMaxLifetime)
|
||||||
xormEngine.SetDefaultContext(ctx)
|
xormEngine.SetDefaultContext(ctx)
|
||||||
|
|
||||||
|
if setting.Database.SlowQueryTreshold > 0 {
|
||||||
|
xormEngine.AddHook(&SlowQueryHook{
|
||||||
|
Treshold: setting.Database.SlowQueryTreshold,
|
||||||
|
Logger: log.GetLogger("xorm"),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
SetDefaultEngine(ctx, xormEngine)
|
SetDefaultEngine(ctx, xormEngine)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
@ -300,3 +310,21 @@ func SetLogSQL(ctx context.Context, on bool) {
|
||||||
sess.Engine().ShowSQL(on)
|
sess.Engine().ShowSQL(on)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type SlowQueryHook struct {
|
||||||
|
Treshold time.Duration
|
||||||
|
Logger log.Logger
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ contexts.Hook = &SlowQueryHook{}
|
||||||
|
|
||||||
|
func (SlowQueryHook) BeforeProcess(c *contexts.ContextHook) (context.Context, error) {
|
||||||
|
return c.Ctx, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *SlowQueryHook) AfterProcess(c *contexts.ContextHook) error {
|
||||||
|
if c.ExecuteTime >= h.Treshold {
|
||||||
|
h.Logger.Log(8, log.WARN, "[Slow SQL Query] %s %v - %v", c.SQL, c.Args, c.ExecuteTime)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
|
@ -6,15 +6,19 @@ package db_test
|
||||||
import (
|
import (
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"testing"
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
"code.gitea.io/gitea/models/db"
|
"code.gitea.io/gitea/models/db"
|
||||||
issues_model "code.gitea.io/gitea/models/issues"
|
issues_model "code.gitea.io/gitea/models/issues"
|
||||||
"code.gitea.io/gitea/models/unittest"
|
"code.gitea.io/gitea/models/unittest"
|
||||||
|
"code.gitea.io/gitea/modules/log"
|
||||||
"code.gitea.io/gitea/modules/setting"
|
"code.gitea.io/gitea/modules/setting"
|
||||||
|
"code.gitea.io/gitea/modules/test"
|
||||||
|
|
||||||
_ "code.gitea.io/gitea/cmd" // for TestPrimaryKeys
|
_ "code.gitea.io/gitea/cmd" // for TestPrimaryKeys
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
|
"xorm.io/xorm"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestDumpDatabase(t *testing.T) {
|
func TestDumpDatabase(t *testing.T) {
|
||||||
|
@ -85,3 +89,37 @@ func TestPrimaryKeys(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestSlowQuery(t *testing.T) {
|
||||||
|
lc, cleanup := test.NewLogChecker("slow-query")
|
||||||
|
lc.StopMark("[Slow SQL Query]")
|
||||||
|
defer cleanup()
|
||||||
|
|
||||||
|
e := db.GetEngine(db.DefaultContext)
|
||||||
|
engine, ok := e.(*xorm.Engine)
|
||||||
|
assert.True(t, ok)
|
||||||
|
|
||||||
|
// It's not possible to clean this up with XORM, but it's luckily not harmful
|
||||||
|
// to leave around.
|
||||||
|
engine.AddHook(&db.SlowQueryHook{
|
||||||
|
Treshold: time.Second * 10,
|
||||||
|
Logger: log.GetLogger("slow-query"),
|
||||||
|
})
|
||||||
|
|
||||||
|
// NOOP query.
|
||||||
|
e.Exec("SELECT 1 WHERE false;")
|
||||||
|
|
||||||
|
_, stopped := lc.Check(100 * time.Millisecond)
|
||||||
|
assert.False(t, stopped)
|
||||||
|
|
||||||
|
engine.AddHook(&db.SlowQueryHook{
|
||||||
|
Treshold: 0, // Every query should be logged.
|
||||||
|
Logger: log.GetLogger("slow-query"),
|
||||||
|
})
|
||||||
|
|
||||||
|
// NOOP query.
|
||||||
|
e.Exec("SELECT 1 WHERE false;")
|
||||||
|
|
||||||
|
_, stopped = lc.Check(100 * time.Millisecond)
|
||||||
|
assert.True(t, stopped)
|
||||||
|
}
|
||||||
|
|
|
@ -136,3 +136,17 @@
|
||||||
is_prerelease: false
|
is_prerelease: false
|
||||||
is_tag: false
|
is_tag: false
|
||||||
created_unix: 946684803
|
created_unix: 946684803
|
||||||
|
|
||||||
|
- id: 11
|
||||||
|
repo_id: 59
|
||||||
|
publisher_id: 2
|
||||||
|
tag_name: "v1.0"
|
||||||
|
lower_tag_name: "v1.0"
|
||||||
|
target: "main"
|
||||||
|
title: "v1.0"
|
||||||
|
sha1: "d8f53dfb33f6ccf4169c34970b5e747511c18beb"
|
||||||
|
num_commits: 1
|
||||||
|
is_draft: false
|
||||||
|
is_prerelease: false
|
||||||
|
is_tag: false
|
||||||
|
created_unix: 946684803
|
||||||
|
|
|
@ -608,6 +608,38 @@
|
||||||
type: 1
|
type: 1
|
||||||
created_unix: 946684810
|
created_unix: 946684810
|
||||||
|
|
||||||
|
# BEGIN Forgejo [GITEA] Improve HTML title on repositories
|
||||||
|
-
|
||||||
|
id: 1093
|
||||||
|
repo_id: 59
|
||||||
|
type: 1
|
||||||
|
created_unix: 946684810
|
||||||
|
|
||||||
|
-
|
||||||
|
id: 1094
|
||||||
|
repo_id: 59
|
||||||
|
type: 2
|
||||||
|
created_unix: 946684810
|
||||||
|
|
||||||
|
-
|
||||||
|
id: 1095
|
||||||
|
repo_id: 59
|
||||||
|
type: 3
|
||||||
|
created_unix: 946684810
|
||||||
|
|
||||||
|
-
|
||||||
|
id: 1096
|
||||||
|
repo_id: 59
|
||||||
|
type: 4
|
||||||
|
created_unix: 946684810
|
||||||
|
|
||||||
|
-
|
||||||
|
id: 1097
|
||||||
|
repo_id: 59
|
||||||
|
type: 5
|
||||||
|
created_unix: 946684810
|
||||||
|
# END Forgejo [GITEA] Improve HTML title on repositories
|
||||||
|
|
||||||
-
|
-
|
||||||
id: 91
|
id: 91
|
||||||
repo_id: 58
|
repo_id: 58
|
||||||
|
|
|
@ -1467,6 +1467,7 @@
|
||||||
owner_name: user27
|
owner_name: user27
|
||||||
lower_name: repo49
|
lower_name: repo49
|
||||||
name: repo49
|
name: repo49
|
||||||
|
description: A wonderful repository with more than just a README.md
|
||||||
default_branch: master
|
default_branch: master
|
||||||
num_watches: 0
|
num_watches: 0
|
||||||
num_stars: 0
|
num_stars: 0
|
||||||
|
@ -1693,3 +1694,16 @@
|
||||||
size: 0
|
size: 0
|
||||||
is_fsck_enabled: true
|
is_fsck_enabled: true
|
||||||
close_issues_via_commit_in_any_branch: false
|
close_issues_via_commit_in_any_branch: false
|
||||||
|
|
||||||
|
-
|
||||||
|
id: 59
|
||||||
|
owner_id: 2
|
||||||
|
owner_name: user2
|
||||||
|
lower_name: repo59
|
||||||
|
name: repo59
|
||||||
|
default_branch: master
|
||||||
|
is_empty: false
|
||||||
|
is_archived: false
|
||||||
|
is_private: false
|
||||||
|
status: 0
|
||||||
|
num_issues: 0
|
||||||
|
|
|
@ -66,7 +66,7 @@
|
||||||
num_followers: 2
|
num_followers: 2
|
||||||
num_following: 1
|
num_following: 1
|
||||||
num_stars: 2
|
num_stars: 2
|
||||||
num_repos: 14
|
num_repos: 15
|
||||||
num_teams: 0
|
num_teams: 0
|
||||||
num_members: 0
|
num_members: 0
|
||||||
visibility: 0
|
visibility: 0
|
||||||
|
|
|
@ -138,12 +138,12 @@ func getTestCases() []struct {
|
||||||
{
|
{
|
||||||
name: "AllPublic/PublicRepositoriesOfUserIncludingCollaborative",
|
name: "AllPublic/PublicRepositoriesOfUserIncludingCollaborative",
|
||||||
opts: &repo_model.SearchRepoOptions{ListOptions: db.ListOptions{Page: 1, PageSize: 10}, OwnerID: 15, AllPublic: true, Template: util.OptionalBoolFalse},
|
opts: &repo_model.SearchRepoOptions{ListOptions: db.ListOptions{Page: 1, PageSize: 10}, OwnerID: 15, AllPublic: true, Template: util.OptionalBoolFalse},
|
||||||
count: 31,
|
count: 32,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "AllPublic/PublicAndPrivateRepositoriesOfUserIncludingCollaborative",
|
name: "AllPublic/PublicAndPrivateRepositoriesOfUserIncludingCollaborative",
|
||||||
opts: &repo_model.SearchRepoOptions{ListOptions: db.ListOptions{Page: 1, PageSize: 10}, OwnerID: 15, Private: true, AllPublic: true, AllLimited: true, Template: util.OptionalBoolFalse},
|
opts: &repo_model.SearchRepoOptions{ListOptions: db.ListOptions{Page: 1, PageSize: 10}, OwnerID: 15, Private: true, AllPublic: true, AllLimited: true, Template: util.OptionalBoolFalse},
|
||||||
count: 36,
|
count: 37,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "AllPublic/PublicAndPrivateRepositoriesOfUserIncludingCollaborativeByName",
|
name: "AllPublic/PublicAndPrivateRepositoriesOfUserIncludingCollaborativeByName",
|
||||||
|
@ -158,7 +158,7 @@ func getTestCases() []struct {
|
||||||
{
|
{
|
||||||
name: "AllPublic/PublicRepositoriesOfOrganization",
|
name: "AllPublic/PublicRepositoriesOfOrganization",
|
||||||
opts: &repo_model.SearchRepoOptions{ListOptions: db.ListOptions{Page: 1, PageSize: 10}, OwnerID: 17, AllPublic: true, Collaborate: util.OptionalBoolFalse, Template: util.OptionalBoolFalse},
|
opts: &repo_model.SearchRepoOptions{ListOptions: db.ListOptions{Page: 1, PageSize: 10}, OwnerID: 17, AllPublic: true, Collaborate: util.OptionalBoolFalse, Template: util.OptionalBoolFalse},
|
||||||
count: 31,
|
count: 32,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "AllTemplates",
|
name: "AllTemplates",
|
||||||
|
|
|
@ -153,18 +153,30 @@ func (r *Writer) WriteRegularLink(l org.RegularLink) {
|
||||||
link = []byte(util.URLJoin(r.URLPrefix, lnk))
|
link = []byte(util.URLJoin(r.URLPrefix, lnk))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
switch l.Kind() {
|
||||||
|
case "image":
|
||||||
|
if l.Description == nil {
|
||||||
|
imageSrc := getMediaURL(link)
|
||||||
|
fmt.Fprintf(r, `<img src="%s" alt="%s" title="%s" />`, imageSrc, link, link)
|
||||||
|
} else {
|
||||||
|
description := strings.TrimPrefix(org.String(l.Description...), "file:")
|
||||||
|
imageSrc := getMediaURL([]byte(description))
|
||||||
|
fmt.Fprintf(r, `<a href="%s"><img src="%s" alt="%s" /></a>`, link, imageSrc, imageSrc)
|
||||||
|
}
|
||||||
|
case "video":
|
||||||
|
if l.Description == nil {
|
||||||
|
imageSrc := getMediaURL(link)
|
||||||
|
fmt.Fprintf(r, `<video src="%s" title="%s">%s</video>`, imageSrc, link, link)
|
||||||
|
} else {
|
||||||
|
description := strings.TrimPrefix(org.String(l.Description...), "file:")
|
||||||
|
videoSrc := getMediaURL([]byte(description))
|
||||||
|
fmt.Fprintf(r, `<a href="%s"><video src="%s" title="%s"></video></a>`, link, videoSrc, videoSrc)
|
||||||
|
}
|
||||||
|
default:
|
||||||
description := string(link)
|
description := string(link)
|
||||||
if l.Description != nil {
|
if l.Description != nil {
|
||||||
description = r.WriteNodesAsString(l.Description...)
|
description = r.WriteNodesAsString(l.Description...)
|
||||||
}
|
}
|
||||||
switch l.Kind() {
|
|
||||||
case "image":
|
|
||||||
imageSrc := getMediaURL(link)
|
|
||||||
fmt.Fprintf(r, `<img src="%s" alt="%s" title="%s" />`, imageSrc, description, description)
|
|
||||||
case "video":
|
|
||||||
videoSrc := getMediaURL(link)
|
|
||||||
fmt.Fprintf(r, `<video src="%s" title="%s">%s</video>`, videoSrc, description, description)
|
|
||||||
default:
|
|
||||||
fmt.Fprintf(r, `<a href="%s" title="%s">%s</a>`, link, description, description)
|
fmt.Fprintf(r, `<a href="%s" title="%s">%s</a>`, link, description, description)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -42,7 +42,7 @@ func TestRender_StandardLinks(t *testing.T) {
|
||||||
"<p><a href=\""+lnk+"\" title=\"WikiPage\">WikiPage</a></p>")
|
"<p><a href=\""+lnk+"\" title=\"WikiPage\">WikiPage</a></p>")
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestRender_Images(t *testing.T) {
|
func TestRender_Media(t *testing.T) {
|
||||||
setting.AppURL = AppURL
|
setting.AppURL = AppURL
|
||||||
setting.AppSubURL = AppSubURL
|
setting.AppSubURL = AppSubURL
|
||||||
|
|
||||||
|
@ -60,6 +60,18 @@ func TestRender_Images(t *testing.T) {
|
||||||
|
|
||||||
test("[[file:"+url+"]]",
|
test("[[file:"+url+"]]",
|
||||||
"<p><img src=\""+result+"\" alt=\""+result+"\" title=\""+result+"\" /></p>")
|
"<p><img src=\""+result+"\" alt=\""+result+"\" title=\""+result+"\" /></p>")
|
||||||
|
|
||||||
|
// With description.
|
||||||
|
test("[[https://example.com][https://example.com/example.svg]]",
|
||||||
|
`<p><a href="https://example.com"><img src="https://example.com/example.svg" alt="https://example.com/example.svg" /></a></p>`)
|
||||||
|
test("[[https://example.com][https://example.com/example.mp4]]",
|
||||||
|
`<p><a href="https://example.com"><video src="https://example.com/example.mp4" title="https://example.com/example.mp4"></video></a></p>`)
|
||||||
|
|
||||||
|
// Without description.
|
||||||
|
test("[[https://example.com/example.svg]]",
|
||||||
|
`<p><img src="https://example.com/example.svg" alt="https://example.com/example.svg" title="https://example.com/example.svg" /></p>`)
|
||||||
|
test("[[https://example.com/example.mp4]]",
|
||||||
|
`<p><video src="https://example.com/example.mp4" title="https://example.com/example.mp4">https://example.com/example.mp4</video></p>`)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestRender_Source(t *testing.T) {
|
func TestRender_Source(t *testing.T) {
|
||||||
|
|
|
@ -7,6 +7,7 @@ package setting
|
||||||
var Admin struct {
|
var Admin struct {
|
||||||
DisableRegularOrgCreation bool
|
DisableRegularOrgCreation bool
|
||||||
DefaultEmailNotification string
|
DefaultEmailNotification string
|
||||||
|
NotifyNewSignUps bool
|
||||||
}
|
}
|
||||||
|
|
||||||
func loadAdminFrom(rootCfg ConfigProvider) {
|
func loadAdminFrom(rootCfg ConfigProvider) {
|
||||||
|
|
|
@ -44,6 +44,7 @@ var (
|
||||||
ConnMaxLifetime time.Duration
|
ConnMaxLifetime time.Duration
|
||||||
IterateBufferSize int
|
IterateBufferSize int
|
||||||
AutoMigration bool
|
AutoMigration bool
|
||||||
|
SlowQueryTreshold time.Duration
|
||||||
}{
|
}{
|
||||||
Timeout: 500,
|
Timeout: 500,
|
||||||
IterateBufferSize: 50,
|
IterateBufferSize: 50,
|
||||||
|
@ -86,6 +87,7 @@ func loadDBSetting(rootCfg ConfigProvider) {
|
||||||
Database.DBConnectRetries = sec.Key("DB_RETRIES").MustInt(10)
|
Database.DBConnectRetries = sec.Key("DB_RETRIES").MustInt(10)
|
||||||
Database.DBConnectBackoff = sec.Key("DB_RETRY_BACKOFF").MustDuration(3 * time.Second)
|
Database.DBConnectBackoff = sec.Key("DB_RETRY_BACKOFF").MustDuration(3 * time.Second)
|
||||||
Database.AutoMigration = sec.Key("AUTO_MIGRATION").MustBool(true)
|
Database.AutoMigration = sec.Key("AUTO_MIGRATION").MustBool(true)
|
||||||
|
Database.SlowQueryTreshold = sec.Key("SLOW_QUERY_TRESHOLD").MustDuration(5 * time.Second)
|
||||||
}
|
}
|
||||||
|
|
||||||
// DBConnStr returns database connection string
|
// DBConnStr returns database connection string
|
||||||
|
|
|
@ -68,6 +68,7 @@ var Service = struct {
|
||||||
DefaultKeepEmailPrivate bool
|
DefaultKeepEmailPrivate bool
|
||||||
DefaultAllowCreateOrganization bool
|
DefaultAllowCreateOrganization bool
|
||||||
DefaultUserIsRestricted bool
|
DefaultUserIsRestricted bool
|
||||||
|
AllowDotsInUsernames bool
|
||||||
EnableTimetracking bool
|
EnableTimetracking bool
|
||||||
DefaultEnableTimetracking bool
|
DefaultEnableTimetracking bool
|
||||||
DefaultEnableDependencies bool
|
DefaultEnableDependencies bool
|
||||||
|
@ -180,6 +181,7 @@ func loadServiceFrom(rootCfg ConfigProvider) {
|
||||||
Service.DefaultKeepEmailPrivate = sec.Key("DEFAULT_KEEP_EMAIL_PRIVATE").MustBool()
|
Service.DefaultKeepEmailPrivate = sec.Key("DEFAULT_KEEP_EMAIL_PRIVATE").MustBool()
|
||||||
Service.DefaultAllowCreateOrganization = sec.Key("DEFAULT_ALLOW_CREATE_ORGANIZATION").MustBool(true)
|
Service.DefaultAllowCreateOrganization = sec.Key("DEFAULT_ALLOW_CREATE_ORGANIZATION").MustBool(true)
|
||||||
Service.DefaultUserIsRestricted = sec.Key("DEFAULT_USER_IS_RESTRICTED").MustBool(false)
|
Service.DefaultUserIsRestricted = sec.Key("DEFAULT_USER_IS_RESTRICTED").MustBool(false)
|
||||||
|
Service.AllowDotsInUsernames = sec.Key("ALLOW_DOTS_IN_USERNAMES").MustBool(true)
|
||||||
Service.EnableTimetracking = sec.Key("ENABLE_TIMETRACKING").MustBool(true)
|
Service.EnableTimetracking = sec.Key("ENABLE_TIMETRACKING").MustBool(true)
|
||||||
if Service.EnableTimetracking {
|
if Service.EnableTimetracking {
|
||||||
Service.DefaultEnableTimetracking = sec.Key("DEFAULT_ENABLE_TIMETRACKING").MustBool(true)
|
Service.DefaultEnableTimetracking = sec.Key("DEFAULT_ENABLE_TIMETRACKING").MustBool(true)
|
||||||
|
|
|
@ -71,6 +71,11 @@ func convertMinioErr(err error) error {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var getBucketVersioning = func(ctx context.Context, minioClient *minio.Client, bucket string) error {
|
||||||
|
_, err := minioClient.GetBucketVersioning(ctx, bucket)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
// NewMinioStorage returns a minio storage
|
// NewMinioStorage returns a minio storage
|
||||||
func NewMinioStorage(ctx context.Context, cfg *setting.Storage) (ObjectStorage, error) {
|
func NewMinioStorage(ctx context.Context, cfg *setting.Storage) (ObjectStorage, error) {
|
||||||
config := cfg.MinioConfig
|
config := cfg.MinioConfig
|
||||||
|
@ -90,6 +95,19 @@ func NewMinioStorage(ctx context.Context, cfg *setting.Storage) (ObjectStorage,
|
||||||
return nil, convertMinioErr(err)
|
return nil, convertMinioErr(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check if the connection works
|
||||||
|
err = getBucketVersioning(ctx, minioClient, config.Bucket)
|
||||||
|
if err != nil {
|
||||||
|
errResp, ok := err.(minio.ErrorResponse)
|
||||||
|
if !ok {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if errResp.StatusCode == http.StatusBadRequest {
|
||||||
|
log.Error("S3 storage connection failure at %s:%s with base path %s and region: %s", config.Endpoint, config.Bucket, config.Location, errResp.Message)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Check to see if we already own this bucket
|
// Check to see if we already own this bucket
|
||||||
exists, err := minioClient.BucketExists(ctx, config.Bucket)
|
exists, err := minioClient.BucketExists(ctx, config.Bucket)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
|
@ -4,10 +4,17 @@
|
||||||
package storage
|
package storage
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
"testing"
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
"code.gitea.io/gitea/modules/setting"
|
"code.gitea.io/gitea/modules/setting"
|
||||||
|
"code.gitea.io/gitea/modules/test"
|
||||||
|
|
||||||
|
"github.com/minio/minio-go/v7"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestMinioStorageIterator(t *testing.T) {
|
func TestMinioStorageIterator(t *testing.T) {
|
||||||
|
@ -25,3 +32,35 @@ func TestMinioStorageIterator(t *testing.T) {
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestS3StorageBadRequest(t *testing.T) {
|
||||||
|
if os.Getenv("CI") == "" {
|
||||||
|
t.Skip("S3Storage not present outside of CI")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
lc, cleanup := test.NewLogChecker("bad-request")
|
||||||
|
lc.StopMark("S3 storage connection failure")
|
||||||
|
defer cleanup()
|
||||||
|
cfg := &setting.Storage{
|
||||||
|
MinioConfig: setting.MinioStorageConfig{
|
||||||
|
Endpoint: "minio:9000",
|
||||||
|
AccessKeyID: "123456",
|
||||||
|
SecretAccessKey: "12345678",
|
||||||
|
Bucket: "bucket",
|
||||||
|
Location: "us-east-1",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
message := "ERROR"
|
||||||
|
defer test.MockVariableValue(&getBucketVersioning, func(ctx context.Context, minioClient *minio.Client, bucket string) error {
|
||||||
|
return minio.ErrorResponse{
|
||||||
|
StatusCode: http.StatusBadRequest,
|
||||||
|
Code: "FixtureError",
|
||||||
|
Message: message,
|
||||||
|
}
|
||||||
|
})()
|
||||||
|
_, err := NewStorage(setting.MinioStorageType, cfg)
|
||||||
|
assert.ErrorContains(t, err, message)
|
||||||
|
|
||||||
|
_, stopped := lc.Check(100 * time.Millisecond)
|
||||||
|
assert.False(t, stopped)
|
||||||
|
}
|
||||||
|
|
|
@ -117,13 +117,20 @@ func IsValidExternalTrackerURLFormat(uri string) bool {
|
||||||
}
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
validUsernamePattern = regexp.MustCompile(`^[\da-zA-Z][-.\w]*$`)
|
validUsernamePatternWithDots = regexp.MustCompile(`^[\da-zA-Z][-.\w]*$`)
|
||||||
invalidUsernamePattern = regexp.MustCompile(`[-._]{2,}|[-._]$`) // No consecutive or trailing non-alphanumeric chars
|
validUsernamePatternWithoutDots = regexp.MustCompile(`^[\da-zA-Z][-\w]*$`)
|
||||||
|
|
||||||
|
// No consecutive or trailing non-alphanumeric chars, catches both cases
|
||||||
|
invalidUsernamePattern = regexp.MustCompile(`[-._]{2,}|[-._]$`)
|
||||||
)
|
)
|
||||||
|
|
||||||
// IsValidUsername checks if username is valid
|
// IsValidUsername checks if username is valid
|
||||||
func IsValidUsername(name string) bool {
|
func IsValidUsername(name string) bool {
|
||||||
// It is difficult to find a single pattern that is both readable and effective,
|
// It is difficult to find a single pattern that is both readable and effective,
|
||||||
// but it's easier to use positive and negative checks.
|
// but it's easier to use positive and negative checks.
|
||||||
return validUsernamePattern.MatchString(name) && !invalidUsernamePattern.MatchString(name)
|
if setting.Service.AllowDotsInUsernames {
|
||||||
|
return validUsernamePatternWithDots.MatchString(name) && !invalidUsernamePattern.MatchString(name)
|
||||||
|
}
|
||||||
|
|
||||||
|
return validUsernamePatternWithoutDots.MatchString(name) && !invalidUsernamePattern.MatchString(name)
|
||||||
}
|
}
|
||||||
|
|
|
@ -155,7 +155,8 @@ func Test_IsValidExternalTrackerURLFormat(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestIsValidUsername(t *testing.T) {
|
func TestIsValidUsernameAllowDots(t *testing.T) {
|
||||||
|
setting.Service.AllowDotsInUsernames = true
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
arg string
|
arg string
|
||||||
want bool
|
want bool
|
||||||
|
@ -185,3 +186,31 @@ func TestIsValidUsername(t *testing.T) {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestIsValidUsernameBanDots(t *testing.T) {
|
||||||
|
setting.Service.AllowDotsInUsernames = false
|
||||||
|
defer func() {
|
||||||
|
setting.Service.AllowDotsInUsernames = true
|
||||||
|
}()
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
arg string
|
||||||
|
want bool
|
||||||
|
}{
|
||||||
|
{arg: "a", want: true},
|
||||||
|
{arg: "abc", want: true},
|
||||||
|
{arg: "0.b-c", want: false},
|
||||||
|
{arg: "a.b-c_d", want: false},
|
||||||
|
{arg: ".abc", want: false},
|
||||||
|
{arg: "abc.", want: false},
|
||||||
|
{arg: "a..bc", want: false},
|
||||||
|
{arg: "a...bc", want: false},
|
||||||
|
{arg: "a.-bc", want: false},
|
||||||
|
{arg: "a._bc", want: false},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.arg, func(t *testing.T) {
|
||||||
|
assert.Equalf(t, tt.want, IsValidUsername(tt.arg), "IsValidUsername[AllowDotsInUsernames=false](%v)", tt.arg)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -8,6 +8,7 @@ import (
|
||||||
"reflect"
|
"reflect"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"code.gitea.io/gitea/modules/setting"
|
||||||
"code.gitea.io/gitea/modules/translation"
|
"code.gitea.io/gitea/modules/translation"
|
||||||
"code.gitea.io/gitea/modules/util"
|
"code.gitea.io/gitea/modules/util"
|
||||||
"code.gitea.io/gitea/modules/validation"
|
"code.gitea.io/gitea/modules/validation"
|
||||||
|
@ -135,7 +136,11 @@ func Validate(errs binding.Errors, data map[string]any, f Form, l translation.Lo
|
||||||
case validation.ErrRegexPattern:
|
case validation.ErrRegexPattern:
|
||||||
data["ErrorMsg"] = trName + l.Tr("form.regex_pattern_error", errs[0].Message)
|
data["ErrorMsg"] = trName + l.Tr("form.regex_pattern_error", errs[0].Message)
|
||||||
case validation.ErrUsername:
|
case validation.ErrUsername:
|
||||||
|
if setting.Service.AllowDotsInUsernames {
|
||||||
data["ErrorMsg"] = trName + l.Tr("form.username_error")
|
data["ErrorMsg"] = trName + l.Tr("form.username_error")
|
||||||
|
} else {
|
||||||
|
data["ErrorMsg"] = trName + l.Tr("form.username_error_no_dots")
|
||||||
|
}
|
||||||
case validation.ErrInvalidGroupTeamMap:
|
case validation.ErrInvalidGroupTeamMap:
|
||||||
data["ErrorMsg"] = trName + l.Tr("form.invalid_group_team_map_error", errs[0].Message)
|
data["ErrorMsg"] = trName + l.Tr("form.invalid_group_team_map_error", errs[0].Message)
|
||||||
default:
|
default:
|
||||||
|
|
|
@ -292,6 +292,7 @@ default_allow_create_organization = Allow Creation of Organizations by Default
|
||||||
default_allow_create_organization_popup = Allow new user accounts to create organizations by default.
|
default_allow_create_organization_popup = Allow new user accounts to create organizations by default.
|
||||||
default_enable_timetracking = Enable Time Tracking by Default
|
default_enable_timetracking = Enable Time Tracking by Default
|
||||||
default_enable_timetracking_popup = Enable time tracking for new repositories by default.
|
default_enable_timetracking_popup = Enable time tracking for new repositories by default.
|
||||||
|
allow_dots_in_usernames = Allow users to use dots in their usernames. Doesn't affect existing accounts.
|
||||||
no_reply_address = Hidden Email Domain
|
no_reply_address = Hidden Email Domain
|
||||||
no_reply_address_helper = Domain name for users with a hidden email address. For example, the username 'joe' will be logged in Git as 'joe@noreply.example.org' if the hidden email domain is set to 'noreply.example.org'.
|
no_reply_address_helper = Domain name for users with a hidden email address. For example, the username 'joe' will be logged in Git as 'joe@noreply.example.org' if the hidden email domain is set to 'noreply.example.org'.
|
||||||
password_algorithm = Password Hash Algorithm
|
password_algorithm = Password Hash Algorithm
|
||||||
|
@ -438,6 +439,10 @@ activate_email = Verify your email address
|
||||||
activate_email.title = %s, please verify your email address
|
activate_email.title = %s, please verify your email address
|
||||||
activate_email.text = Please click the following link to verify your email address within <b>%s</b>:
|
activate_email.text = Please click the following link to verify your email address within <b>%s</b>:
|
||||||
|
|
||||||
|
admin.new_user.subject = New user %s
|
||||||
|
admin.new_user.user_info = User Information
|
||||||
|
admin.new_user.text = Please <a href="%s">click here</a> to manage the user from the admin panel.
|
||||||
|
|
||||||
register_notify = Welcome to Gitea
|
register_notify = Welcome to Gitea
|
||||||
register_notify.title = %[1]s, welcome to %[2]s
|
register_notify.title = %[1]s, welcome to %[2]s
|
||||||
register_notify.text_1 = this is your registration confirmation email for %s!
|
register_notify.text_1 = this is your registration confirmation email for %s!
|
||||||
|
@ -532,6 +537,7 @@ include_error = ` must contain substring "%s".`
|
||||||
glob_pattern_error = ` glob pattern is invalid: %s.`
|
glob_pattern_error = ` glob pattern is invalid: %s.`
|
||||||
regex_pattern_error = ` regex pattern is invalid: %s.`
|
regex_pattern_error = ` regex pattern is invalid: %s.`
|
||||||
username_error = ` can only contain alphanumeric chars ('0-9','a-z','A-Z'), dash ('-'), underscore ('_') and dot ('.'). It cannot begin or end with non-alphanumeric chars, and consecutive non-alphanumeric chars are also forbidden.`
|
username_error = ` can only contain alphanumeric chars ('0-9','a-z','A-Z'), dash ('-'), underscore ('_') and dot ('.'). It cannot begin or end with non-alphanumeric chars, and consecutive non-alphanumeric chars are also forbidden.`
|
||||||
|
username_error_no_dots = ` can only contain alphanumeric chars ('0-9','a-z','A-Z'), dash ('-') and underscore ('_'). It cannot begin or end with non-alphanumeric chars, and consecutive non-alphanumeric chars are also forbidden.`
|
||||||
invalid_group_team_map_error = ` mapping is invalid: %s`
|
invalid_group_team_map_error = ` mapping is invalid: %s`
|
||||||
unknown_error = Unknown error:
|
unknown_error = Unknown error:
|
||||||
captcha_incorrect = The CAPTCHA code is incorrect.
|
captcha_incorrect = The CAPTCHA code is incorrect.
|
||||||
|
|
|
@ -358,6 +358,12 @@ func SubmitInstall(ctx *context.Context) {
|
||||||
ctx.RenderWithErr(ctx.Tr("form.password_not_match"), tplInstall, form)
|
ctx.RenderWithErr(ctx.Tr("form.password_not_match"), tplInstall, form)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if len(form.AdminPasswd) < setting.MinPasswordLength {
|
||||||
|
ctx.Data["Err_Admin"] = true
|
||||||
|
ctx.Data["Err_AdminPasswd"] = true
|
||||||
|
ctx.RenderWithErr(ctx.Tr("auth.password_too_short", setting.MinPasswordLength), tplInstall, form)
|
||||||
|
return
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Init the engine with migration
|
// Init the engine with migration
|
||||||
|
|
|
@ -30,6 +30,7 @@ import (
|
||||||
"code.gitea.io/gitea/services/externalaccount"
|
"code.gitea.io/gitea/services/externalaccount"
|
||||||
"code.gitea.io/gitea/services/forms"
|
"code.gitea.io/gitea/services/forms"
|
||||||
"code.gitea.io/gitea/services/mailer"
|
"code.gitea.io/gitea/services/mailer"
|
||||||
|
notify_service "code.gitea.io/gitea/services/notify"
|
||||||
|
|
||||||
"github.com/markbates/goth"
|
"github.com/markbates/goth"
|
||||||
)
|
)
|
||||||
|
@ -568,6 +569,7 @@ func handleUserCreated(ctx *context.Context, u *user_model.User, gothUser *goth.
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
notify_service.NewUserSignUp(ctx, u)
|
||||||
// update external user information
|
// update external user information
|
||||||
if gothUser != nil {
|
if gothUser != nil {
|
||||||
if err := externalaccount.UpdateExternalUser(u, *gothUser); err != nil {
|
if err := externalaccount.UpdateExternalUser(u, *gothUser); err != nil {
|
||||||
|
@ -591,7 +593,6 @@ func handleUserCreated(ctx *context.Context, u *user_model.User, gothUser *goth.
|
||||||
ctx.Data["Email"] = u.Email
|
ctx.Data["Email"] = u.Email
|
||||||
ctx.Data["ActiveCodeLives"] = timeutil.MinutesToFriendly(setting.Service.ActiveCodeLives, ctx.Locale)
|
ctx.Data["ActiveCodeLives"] = timeutil.MinutesToFriendly(setting.Service.ActiveCodeLives, ctx.Locale)
|
||||||
ctx.HTML(http.StatusOK, TplActivate)
|
ctx.HTML(http.StatusOK, TplActivate)
|
||||||
|
|
||||||
if setting.CacheService.Enabled {
|
if setting.CacheService.Enabled {
|
||||||
if err := ctx.Cache.Put("MailResendLimit_"+u.LowerName, u.LowerName, 180); err != nil {
|
if err := ctx.Cache.Put("MailResendLimit_"+u.LowerName, u.LowerName, 180); err != nil {
|
||||||
log.Error("Set cache(MailResendLimit) fail: %v", err)
|
log.Error("Set cache(MailResendLimit) fail: %v", err)
|
||||||
|
|
|
@ -387,7 +387,9 @@ func NewReleasePost(ctx *context.Context) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if !ctx.Repo.GitRepo.IsBranchExist(form.Target) {
|
// form.Target can be a branch name or a full commitID.
|
||||||
|
if !ctx.Repo.GitRepo.IsBranchExist(form.Target) &&
|
||||||
|
len(form.Target) == git.SHAFullLength && !ctx.Repo.GitRepo.IsCommitExist(form.Target) {
|
||||||
ctx.RenderWithErr(ctx.Tr("form.target_branch_not_exist"), tplReleaseNew, &form)
|
ctx.RenderWithErr(ctx.Tr("form.target_branch_not_exist"), tplReleaseNew, &form)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
|
@ -165,7 +165,7 @@ func renderDirectory(ctx *context.Context, treeLink string) {
|
||||||
|
|
||||||
if ctx.Repo.TreePath != "" {
|
if ctx.Repo.TreePath != "" {
|
||||||
ctx.Data["HideRepoInfo"] = true
|
ctx.Data["HideRepoInfo"] = true
|
||||||
ctx.Data["Title"] = ctx.Tr("repo.file.title", ctx.Repo.Repository.Name+"/"+path.Base(ctx.Repo.TreePath), ctx.Repo.RefName)
|
ctx.Data["Title"] = ctx.Tr("repo.file.title", ctx.Repo.Repository.Name+"/"+util.PathEscapeSegments(ctx.Repo.TreePath), ctx.Repo.RefName)
|
||||||
}
|
}
|
||||||
|
|
||||||
subfolder, readmeFile, err := findReadmeFileInEntries(ctx, entries, true)
|
subfolder, readmeFile, err := findReadmeFileInEntries(ctx, entries, true)
|
||||||
|
@ -344,7 +344,7 @@ func renderFile(ctx *context.Context, entry *git.TreeEntry, treeLink, rawLink st
|
||||||
}
|
}
|
||||||
defer dataRc.Close()
|
defer dataRc.Close()
|
||||||
|
|
||||||
ctx.Data["Title"] = ctx.Tr("repo.file.title", ctx.Repo.Repository.Name+"/"+path.Base(ctx.Repo.TreePath), ctx.Repo.RefName)
|
ctx.Data["Title"] = ctx.Tr("repo.file.title", ctx.Repo.Repository.Name+"/"+util.PathEscapeSegments(ctx.Repo.TreePath), ctx.Repo.RefName)
|
||||||
ctx.Data["FileIsSymlink"] = entry.IsLink()
|
ctx.Data["FileIsSymlink"] = entry.IsLink()
|
||||||
ctx.Data["FileName"] = blob.Name()
|
ctx.Data["FileName"] = blob.Name()
|
||||||
ctx.Data["RawFileLink"] = rawLink + "/" + util.PathEscapeSegments(ctx.Repo.TreePath)
|
ctx.Data["RawFileLink"] = rawLink + "/" + util.PathEscapeSegments(ctx.Repo.TreePath)
|
||||||
|
|
|
@ -106,6 +106,12 @@ func ListTasks() TaskTable {
|
||||||
next = e.NextRun()
|
next = e.NextRun()
|
||||||
prev = e.PreviousRun()
|
prev = e.PreviousRun()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// If the manual run is after the cron run, use that instead.
|
||||||
|
if prev.Before(task.LastRun) {
|
||||||
|
prev = task.LastRun
|
||||||
|
}
|
||||||
|
|
||||||
task.lock.Lock()
|
task.lock.Lock()
|
||||||
tTable = append(tTable, &TaskTableRow{
|
tTable = append(tTable, &TaskTableRow{
|
||||||
Name: task.Name,
|
Name: task.Name,
|
||||||
|
|
|
@ -9,6 +9,7 @@ import (
|
||||||
"reflect"
|
"reflect"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
"code.gitea.io/gitea/models/db"
|
"code.gitea.io/gitea/models/db"
|
||||||
system_model "code.gitea.io/gitea/models/system"
|
system_model "code.gitea.io/gitea/models/system"
|
||||||
|
@ -37,6 +38,8 @@ type Task struct {
|
||||||
LastMessage string
|
LastMessage string
|
||||||
LastDoer string
|
LastDoer string
|
||||||
ExecTimes int64
|
ExecTimes int64
|
||||||
|
// This stores the time of the last manual run of this task.
|
||||||
|
LastRun time.Time
|
||||||
}
|
}
|
||||||
|
|
||||||
// DoRunAtStart returns if this task should run at the start
|
// DoRunAtStart returns if this task should run at the start
|
||||||
|
@ -88,6 +91,12 @@ func (t *Task) RunWithUser(doer *user_model.User, config Config) {
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
graceful.GetManager().RunWithShutdownContext(func(baseCtx context.Context) {
|
graceful.GetManager().RunWithShutdownContext(func(baseCtx context.Context) {
|
||||||
|
// Store the time of this run, before the function is executed, so it
|
||||||
|
// matches the behavior of what the cron library does.
|
||||||
|
t.lock.Lock()
|
||||||
|
t.LastRun = time.Now()
|
||||||
|
t.lock.Unlock()
|
||||||
|
|
||||||
pm := process.GetManager()
|
pm := process.GetManager()
|
||||||
doerName := ""
|
doerName := ""
|
||||||
if doer != nil && doer.ID != -1 {
|
if doer != nil && doer.ID != -1 {
|
||||||
|
|
82
services/mailer/mail_admin_new_user.go
Normal file
82
services/mailer/mail_admin_new_user.go
Normal file
|
@ -0,0 +1,82 @@
|
||||||
|
// Copyright 2023 The Forgejo Authors. All rights reserved.
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
package mailer
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
user_model "code.gitea.io/gitea/models/user"
|
||||||
|
"code.gitea.io/gitea/modules/base"
|
||||||
|
"code.gitea.io/gitea/modules/log"
|
||||||
|
"code.gitea.io/gitea/modules/setting"
|
||||||
|
"code.gitea.io/gitea/modules/templates"
|
||||||
|
"code.gitea.io/gitea/modules/translation"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
tplNewUserMail base.TplName = "admin_new_user"
|
||||||
|
)
|
||||||
|
|
||||||
|
var sa = SendAsyncs
|
||||||
|
|
||||||
|
// MailNewUser sends notification emails on new user registrations to all admins
|
||||||
|
func MailNewUser(ctx context.Context, u *user_model.User) {
|
||||||
|
if !setting.Admin.NotifyNewSignUps {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if setting.MailService == nil {
|
||||||
|
// No mail service configured
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
recipients, err := user_model.GetAllUsers()
|
||||||
|
if err != nil {
|
||||||
|
log.Error("user_model.GetAllUsers: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
langMap := make(map[string][]string)
|
||||||
|
for _, r := range recipients {
|
||||||
|
if r.IsAdmin {
|
||||||
|
langMap[r.Language] = append(langMap[r.Language], r.Email)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for lang, tos := range langMap {
|
||||||
|
mailNewUser(ctx, u, lang, tos)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func mailNewUser(ctx context.Context, u *user_model.User, lang string, tos []string) {
|
||||||
|
locale := translation.NewLocale(lang)
|
||||||
|
|
||||||
|
subject := locale.Tr("mail.admin.new_user.subject", u.Name)
|
||||||
|
manageUserURL := setting.AppSubURL + "/admin/users/" + strconv.FormatInt(u.ID, 10)
|
||||||
|
body := locale.Tr("mail.admin.new_user.text", manageUserURL)
|
||||||
|
mailMeta := map[string]any{
|
||||||
|
"NewUser": u,
|
||||||
|
"Subject": subject,
|
||||||
|
"Body": body,
|
||||||
|
"Language": locale.Language(),
|
||||||
|
"locale": locale,
|
||||||
|
"Str2html": templates.Str2html,
|
||||||
|
}
|
||||||
|
|
||||||
|
var mailBody bytes.Buffer
|
||||||
|
|
||||||
|
if err := bodyTemplates.ExecuteTemplate(&mailBody, string(tplNewUserMail), mailMeta); err != nil {
|
||||||
|
log.Error("ExecuteTemplate [%s]: %v", string(tplNewUserMail)+"/body", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
msgs := make([]*Message, 0, len(tos))
|
||||||
|
for _, to := range tos {
|
||||||
|
msg := NewMessage(to, subject, mailBody.String())
|
||||||
|
msg.Info = subject
|
||||||
|
msgs = append(msgs, msg)
|
||||||
|
}
|
||||||
|
sa(msgs)
|
||||||
|
}
|
88
services/mailer/mail_admin_new_user_test.go
Normal file
88
services/mailer/mail_admin_new_user_test.go
Normal file
|
@ -0,0 +1,88 @@
|
||||||
|
// Copyright 2023 The Gitea Authors. All rights reserved.
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
package mailer
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"code.gitea.io/gitea/models/db"
|
||||||
|
user_model "code.gitea.io/gitea/models/user"
|
||||||
|
"code.gitea.io/gitea/modules/setting"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func getTestUsers() []*user_model.User {
|
||||||
|
admin := new(user_model.User)
|
||||||
|
admin.Name = "admin"
|
||||||
|
admin.IsAdmin = true
|
||||||
|
admin.Language = "en_US"
|
||||||
|
admin.Email = "admin@forgejo.org"
|
||||||
|
|
||||||
|
newUser := new(user_model.User)
|
||||||
|
newUser.Name = "new_user"
|
||||||
|
newUser.Language = "en_US"
|
||||||
|
newUser.IsAdmin = false
|
||||||
|
newUser.Email = "new_user@forgejo.org"
|
||||||
|
newUser.LastLoginUnix = 1693648327
|
||||||
|
newUser.CreatedUnix = 1693648027
|
||||||
|
|
||||||
|
user_model.CreateUser(admin)
|
||||||
|
user_model.CreateUser(newUser)
|
||||||
|
|
||||||
|
users := make([]*user_model.User, 0)
|
||||||
|
users = append(users, admin)
|
||||||
|
users = append(users, newUser)
|
||||||
|
|
||||||
|
return users
|
||||||
|
}
|
||||||
|
|
||||||
|
func cleanUpUsers(ctx context.Context, users []*user_model.User) {
|
||||||
|
for _, u := range users {
|
||||||
|
db.DeleteByID(ctx, u.ID, new(user_model.User))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAdminNotificationMail_test(t *testing.T) {
|
||||||
|
mailService := setting.Mailer{
|
||||||
|
From: "test@forgejo.org",
|
||||||
|
Protocol: "dummy",
|
||||||
|
}
|
||||||
|
|
||||||
|
setting.MailService = &mailService
|
||||||
|
setting.Domain = "localhost"
|
||||||
|
setting.AppSubURL = "http://localhost"
|
||||||
|
|
||||||
|
// test with NOTIFY_NEW_SIGNUPS enabled
|
||||||
|
setting.Admin.NotifyNewSignUps = true
|
||||||
|
|
||||||
|
ctx := context.Background()
|
||||||
|
NewContext(ctx)
|
||||||
|
|
||||||
|
users := getTestUsers()
|
||||||
|
oldSendAsyncs := sa
|
||||||
|
defer func() {
|
||||||
|
sa = oldSendAsyncs
|
||||||
|
cleanUpUsers(ctx, users)
|
||||||
|
}()
|
||||||
|
|
||||||
|
sa = func(msgs []*Message) {
|
||||||
|
assert.Equal(t, len(msgs), 1, "Test provides only one admin user, so only one email must be sent")
|
||||||
|
assert.Equal(t, msgs[0].To, users[0].Email, "checks if the recipient is the admin of the instance")
|
||||||
|
manageUserURL := "/admin/users/" + strconv.FormatInt(users[1].ID, 10)
|
||||||
|
assert.True(t, strings.ContainsAny(msgs[0].Body, manageUserURL), "checks if the message contains the link to manage the newly created user from the admin panel")
|
||||||
|
}
|
||||||
|
MailNewUser(ctx, users[1])
|
||||||
|
|
||||||
|
// test with NOTIFY_NEW_SIGNUPS disabled; emails shouldn't be sent
|
||||||
|
setting.Admin.NotifyNewSignUps = false
|
||||||
|
sa = func(msgs []*Message) {
|
||||||
|
assert.Equal(t, 1, 0, "this shouldn't execute. MailNewUser must exit early since NOTIFY_NEW_SIGNUPS is disabled")
|
||||||
|
}
|
||||||
|
|
||||||
|
MailNewUser(ctx, users[1])
|
||||||
|
}
|
|
@ -199,3 +199,7 @@ func (m *mailNotifier) RepoPendingTransfer(ctx context.Context, doer, newOwner *
|
||||||
log.Error("SendRepoTransferNotifyMail: %v", err)
|
log.Error("SendRepoTransferNotifyMail: %v", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (m *mailNotifier) NewUserSignUp(ctx context.Context, newUser *user_model.User) {
|
||||||
|
MailNewUser(ctx, newUser)
|
||||||
|
}
|
||||||
|
|
|
@ -72,4 +72,5 @@ type Notifier interface {
|
||||||
|
|
||||||
PackageCreate(ctx context.Context, doer *user_model.User, pd *packages_model.PackageDescriptor)
|
PackageCreate(ctx context.Context, doer *user_model.User, pd *packages_model.PackageDescriptor)
|
||||||
PackageDelete(ctx context.Context, doer *user_model.User, pd *packages_model.PackageDescriptor)
|
PackageDelete(ctx context.Context, doer *user_model.User, pd *packages_model.PackageDescriptor)
|
||||||
|
NewUserSignUp(ctx context.Context, newUser *user_model.User)
|
||||||
}
|
}
|
||||||
|
|
|
@ -360,3 +360,10 @@ func PackageDelete(ctx context.Context, doer *user_model.User, pd *packages_mode
|
||||||
notifier.PackageDelete(ctx, doer, pd)
|
notifier.PackageDelete(ctx, doer, pd)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// NewUserSignUp notifies deletion of a package to notifiers
|
||||||
|
func NewUserSignUp(ctx context.Context, newUser *user_model.User) {
|
||||||
|
for _, notifier := range notifiers {
|
||||||
|
notifier.NewUserSignUp(ctx, newUser)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -204,3 +204,7 @@ func (*NullNotifier) PackageCreate(ctx context.Context, doer *user_model.User, p
|
||||||
// PackageDelete places a place holder function
|
// PackageDelete places a place holder function
|
||||||
func (*NullNotifier) PackageDelete(ctx context.Context, doer *user_model.User, pd *packages_model.PackageDescriptor) {
|
func (*NullNotifier) PackageDelete(ctx context.Context, doer *user_model.User, pd *packages_model.PackageDescriptor) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// NotifyNewUserSignUp notifies deletion of a package to notifiers
|
||||||
|
func (*NullNotifier) NewUserSignUp(ctx context.Context, newUser *user_model.User) {
|
||||||
|
}
|
||||||
|
|
|
@ -251,8 +251,8 @@ func TestPrepareWikiFileName(t *testing.T) {
|
||||||
unittest.PrepareTestEnv(t)
|
unittest.PrepareTestEnv(t)
|
||||||
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
|
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
|
||||||
gitRepo, err := git.OpenRepository(git.DefaultContext, repo.WikiPath())
|
gitRepo, err := git.OpenRepository(git.DefaultContext, repo.WikiPath())
|
||||||
defer gitRepo.Close()
|
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
|
defer gitRepo.Close()
|
||||||
|
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
|
@ -303,8 +303,8 @@ func TestPrepareWikiFileName_FirstPage(t *testing.T) {
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
|
|
||||||
gitRepo, err := git.OpenRepository(git.DefaultContext, tmpDir)
|
gitRepo, err := git.OpenRepository(git.DefaultContext, tmpDir)
|
||||||
defer gitRepo.Close()
|
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
|
defer gitRepo.Close()
|
||||||
|
|
||||||
existence, newWikiPath, err := prepareGitPath(gitRepo, "Home")
|
existence, newWikiPath, err := prepareGitPath(gitRepo, "Home")
|
||||||
assert.False(t, existence)
|
assert.False(t, existence)
|
||||||
|
|
|
@ -159,6 +159,8 @@
|
||||||
<dd>{{if .Service.DefaultKeepEmailPrivate}}{{svg "octicon-check"}}{{else}}{{svg "octicon-x"}}{{end}}</dd>
|
<dd>{{if .Service.DefaultKeepEmailPrivate}}{{svg "octicon-check"}}{{else}}{{svg "octicon-x"}}{{end}}</dd>
|
||||||
<dt>{{.locale.Tr "admin.config.default_allow_create_organization"}}</dt>
|
<dt>{{.locale.Tr "admin.config.default_allow_create_organization"}}</dt>
|
||||||
<dd>{{if .Service.DefaultAllowCreateOrganization}}{{svg "octicon-check"}}{{else}}{{svg "octicon-x"}}{{end}}</dd>
|
<dd>{{if .Service.DefaultAllowCreateOrganization}}{{svg "octicon-check"}}{{else}}{{svg "octicon-x"}}{{end}}</dd>
|
||||||
|
<dt>{{.locale.Tr "admin.config.allow_dots_in_usernames"}}</dt>
|
||||||
|
<dd>{{if .Service.AllowDotsInUsernames}}{{svg "octicon-check"}}{{else}}{{svg "octicon-x"}}{{end}}</dd>
|
||||||
<dt>{{.locale.Tr "admin.config.enable_timetracking"}}</dt>
|
<dt>{{.locale.Tr "admin.config.enable_timetracking"}}</dt>
|
||||||
<dd>{{if .Service.EnableTimetracking}}{{svg "octicon-check"}}{{else}}{{svg "octicon-x"}}{{end}}</dd>
|
<dd>{{if .Service.EnableTimetracking}}{{svg "octicon-check"}}{{else}}{{svg "octicon-x"}}{{end}}</dd>
|
||||||
{{if .Service.EnableTimetracking}}
|
{{if .Service.EnableTimetracking}}
|
||||||
|
|
|
@ -2,7 +2,8 @@
|
||||||
<html lang="{{ctx.Locale.Lang}}" class="theme-{{if .SignedUser.Theme}}{{.SignedUser.Theme}}{{else}}{{DefaultTheme}}{{end}}">
|
<html lang="{{ctx.Locale.Lang}}" class="theme-{{if .SignedUser.Theme}}{{.SignedUser.Theme}}{{else}}{{DefaultTheme}}{{end}}">
|
||||||
<head>
|
<head>
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
<title>{{if .Title}}{{.Title | RenderEmojiPlain}} - {{end}}{{if .Repository.Name}}{{.Repository.Name}} - {{end}}{{AppName}}</title>
|
{{/* Display `- .Repsository.FullName` only if `.Title` does not already start with that. */}}
|
||||||
|
<title>{{if .Title}}{{.Title | RenderEmojiPlain}} - {{end}}{{if and (.Repository.Name) (not (StringUtils.HasPrefix .Title .Repository.FullName))}}{{.Repository.FullName}} - {{end}}{{AppName}}</title>
|
||||||
{{if .ManifestData}}<link rel="manifest" href="data:{{.ManifestData}}">{{end}}
|
{{if .ManifestData}}<link rel="manifest" href="data:{{.ManifestData}}">{{end}}
|
||||||
<meta name="author" content="{{if .Repository}}{{.Owner.Name}}{{else}}{{MetaAuthor}}{{end}}">
|
<meta name="author" content="{{if .Repository}}{{.Owner.Name}}{{else}}{{MetaAuthor}}{{end}}">
|
||||||
<meta name="description" content="{{if .Repository}}{{.Repository.Name}}{{if .Repository.Description}} - {{.Repository.Description}}{{end}}{{else}}{{MetaDescription}}{{end}}">
|
<meta name="description" content="{{if .Repository}}{{.Repository.Name}}{{if .Repository.Description}} - {{.Repository.Description}}{{end}}{{else}}{{MetaDescription}}{{end}}">
|
||||||
|
|
22
templates/mail/admin_new_user.tmpl
Normal file
22
templates/mail/admin_new_user.tmpl
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
|
||||||
|
<title>{{.Subject}}</title>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
blockquote { padding-left: 1em; margin: 1em 0; border-left: 1px solid grey; color: #777}
|
||||||
|
.footer { font-size:small; color:#666;}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<ul>
|
||||||
|
<h3>{{.locale.Tr "mail.admin.new_user.user_info"}}</h3>
|
||||||
|
<li>{{.locale.Tr "admin.users.created"}}: {{DateTime "full" .NewUser.LastLoginUnix}}</li>
|
||||||
|
<li>{{.locale.Tr "admin.users.last_login"}}: {{DateTime "full" .NewUser.CreatedUnix}}</li>
|
||||||
|
</ul>
|
||||||
|
<p> {{.Body | Str2html}} </p>
|
||||||
|
</body>
|
||||||
|
</html>
|
|
@ -364,7 +364,7 @@
|
||||||
{{end}}
|
{{end}}
|
||||||
</div>
|
</div>
|
||||||
{{else if eq .Type 22}}
|
{{else if eq .Type 22}}
|
||||||
<div class="timeline-item-group">
|
<div class="timeline-item-group" id="{{.HashTag}}">
|
||||||
<div class="timeline-item event">
|
<div class="timeline-item event">
|
||||||
{{if .OriginalAuthor}}
|
{{if .OriginalAuthor}}
|
||||||
{{else}}
|
{{else}}
|
||||||
|
|
1
tests/gitea-repositories-meta/user2/repo59.git/HEAD
Normal file
1
tests/gitea-repositories-meta/user2/repo59.git/HEAD
Normal file
|
@ -0,0 +1 @@
|
||||||
|
ref: refs/heads/master
|
4
tests/gitea-repositories-meta/user2/repo59.git/config
Normal file
4
tests/gitea-repositories-meta/user2/repo59.git/config
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
[core]
|
||||||
|
repositoryformatversion = 0
|
||||||
|
filemode = true
|
||||||
|
bare = true
|
|
@ -0,0 +1 @@
|
||||||
|
Unnamed repository; edit this file 'description' to name the repository.
|
|
@ -0,0 +1,6 @@
|
||||||
|
# git ls-files --others --exclude-from=.git/info/exclude
|
||||||
|
# Lines that start with '#' are comments.
|
||||||
|
# For a project mostly in C, the following would be a good set of
|
||||||
|
# exclude patterns (uncomment them if you want to use them):
|
||||||
|
# *.[oa]
|
||||||
|
# *~
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
@ -0,0 +1,3 @@
|
||||||
|
# pack-refs with: peeled fully-peeled sorted
|
||||||
|
d8f53dfb33f6ccf4169c34970b5e747511c18beb refs/heads/master
|
||||||
|
d8f53dfb33f6ccf4169c34970b5e747511c18beb refs/tags/v1.0
|
|
@ -0,0 +1 @@
|
||||||
|
d8f53dfb33f6ccf4169c34970b5e747511c18beb
|
|
@ -7,6 +7,7 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
"testing"
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
asymkey_model "code.gitea.io/gitea/models/asymkey"
|
asymkey_model "code.gitea.io/gitea/models/asymkey"
|
||||||
auth_model "code.gitea.io/gitea/models/auth"
|
auth_model "code.gitea.io/gitea/models/auth"
|
||||||
|
@ -282,3 +283,54 @@ func TestAPIRenameUser(t *testing.T) {
|
||||||
})
|
})
|
||||||
MakeRequest(t, req, http.StatusOK)
|
MakeRequest(t, req, http.StatusOK)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestAPICron(t *testing.T) {
|
||||||
|
defer tests.PrepareTestEnv(t)()
|
||||||
|
|
||||||
|
// user1 is an admin user
|
||||||
|
session := loginUser(t, "user1")
|
||||||
|
|
||||||
|
t.Run("List", func(t *testing.T) {
|
||||||
|
defer tests.PrintCurrentTest(t)()
|
||||||
|
|
||||||
|
token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeReadAdmin)
|
||||||
|
urlStr := fmt.Sprintf("/api/v1/admin/cron?token=%s", token)
|
||||||
|
req := NewRequest(t, "GET", urlStr)
|
||||||
|
resp := MakeRequest(t, req, http.StatusOK)
|
||||||
|
|
||||||
|
assert.NotEmpty(t, resp.Header().Get("X-Total-Count"))
|
||||||
|
|
||||||
|
var crons []api.Cron
|
||||||
|
DecodeJSON(t, resp, &crons)
|
||||||
|
|
||||||
|
assert.NotEmpty(t, crons)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Execute", func(t *testing.T) {
|
||||||
|
defer tests.PrintCurrentTest(t)()
|
||||||
|
|
||||||
|
now := time.Now()
|
||||||
|
token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteAdmin)
|
||||||
|
/// Archive cleanup is harmless, because in the text environment there are none
|
||||||
|
/// and is thus an NOOP operation and therefore doesn't interfere with any other
|
||||||
|
/// tests.
|
||||||
|
urlStr := fmt.Sprintf("/api/v1/admin/cron/archive_cleanup?token=%s", token)
|
||||||
|
req := NewRequest(t, "POST", urlStr)
|
||||||
|
MakeRequest(t, req, http.StatusNoContent)
|
||||||
|
|
||||||
|
// Check for the latest run time for this cron, to ensure it
|
||||||
|
// has been run.
|
||||||
|
urlStr = fmt.Sprintf("/api/v1/admin/cron?token=%s", token)
|
||||||
|
req = NewRequest(t, "GET", urlStr)
|
||||||
|
resp := MakeRequest(t, req, http.StatusOK)
|
||||||
|
|
||||||
|
var crons []api.Cron
|
||||||
|
DecodeJSON(t, resp, &crons)
|
||||||
|
|
||||||
|
for _, cron := range crons {
|
||||||
|
if cron.Name == "archive_cleanup" {
|
||||||
|
assert.True(t, now.Before(cron.Prev))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
|
@ -93,9 +93,9 @@ func TestAPISearchRepo(t *testing.T) {
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
name: "RepositoriesMax50", requestURL: "/api/v1/repos/search?limit=50&private=false", expectedResults: expectedResults{
|
name: "RepositoriesMax50", requestURL: "/api/v1/repos/search?limit=50&private=false", expectedResults: expectedResults{
|
||||||
nil: {count: 33},
|
nil: {count: 34},
|
||||||
user: {count: 33},
|
user: {count: 34},
|
||||||
user2: {count: 33},
|
user2: {count: 34},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
|
@ -547,3 +547,18 @@ func GetCSRF(t testing.TB, session *TestSession, urlStr string) string {
|
||||||
doc := NewHTMLParser(t, resp.Body)
|
doc := NewHTMLParser(t, resp.Body)
|
||||||
return doc.GetCSRF()
|
return doc.GetCSRF()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func GetHTMLTitle(t testing.TB, session *TestSession, urlStr string) string {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
req := NewRequest(t, "GET", urlStr)
|
||||||
|
var resp *httptest.ResponseRecorder
|
||||||
|
if session == nil {
|
||||||
|
resp = MakeRequest(t, req, http.StatusOK)
|
||||||
|
} else {
|
||||||
|
resp = session.MakeRequest(t, req, http.StatusOK)
|
||||||
|
}
|
||||||
|
|
||||||
|
doc := NewHTMLParser(t, resp.Body)
|
||||||
|
return doc.Find("head title").Text()
|
||||||
|
}
|
||||||
|
|
|
@ -21,6 +21,10 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
func createNewRelease(t *testing.T, session *TestSession, repoURL, tag, title string, preRelease, draft bool) {
|
func createNewRelease(t *testing.T, session *TestSession, repoURL, tag, title string, preRelease, draft bool) {
|
||||||
|
createNewReleaseTarget(t, session, repoURL, tag, title, "master", preRelease, draft)
|
||||||
|
}
|
||||||
|
|
||||||
|
func createNewReleaseTarget(t *testing.T, session *TestSession, repoURL, tag, title, target string, preRelease, draft bool) {
|
||||||
req := NewRequest(t, "GET", repoURL+"/releases/new")
|
req := NewRequest(t, "GET", repoURL+"/releases/new")
|
||||||
resp := session.MakeRequest(t, req, http.StatusOK)
|
resp := session.MakeRequest(t, req, http.StatusOK)
|
||||||
htmlDoc := NewHTMLParser(t, resp.Body)
|
htmlDoc := NewHTMLParser(t, resp.Body)
|
||||||
|
@ -31,7 +35,7 @@ func createNewRelease(t *testing.T, session *TestSession, repoURL, tag, title st
|
||||||
postData := map[string]string{
|
postData := map[string]string{
|
||||||
"_csrf": htmlDoc.GetCSRF(),
|
"_csrf": htmlDoc.GetCSRF(),
|
||||||
"tag_name": tag,
|
"tag_name": tag,
|
||||||
"tag_target": "master",
|
"tag_target": target,
|
||||||
"title": title,
|
"title": title,
|
||||||
"content": "",
|
"content": "",
|
||||||
}
|
}
|
||||||
|
@ -239,3 +243,12 @@ func TestViewTagsList(t *testing.T) {
|
||||||
|
|
||||||
assert.EqualValues(t, []string{"v1.0", "delete-tag", "v1.1"}, tagNames)
|
assert.EqualValues(t, []string{"v1.0", "delete-tag", "v1.1"}, tagNames)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestReleaseOnCommit(t *testing.T) {
|
||||||
|
defer tests.PrepareTestEnv(t)()
|
||||||
|
|
||||||
|
session := loginUser(t, "user2")
|
||||||
|
createNewReleaseTarget(t, session, "/user2/repo1", "v0.0.1", "v0.0.1", "65f1bf27bc3bf70f64657658635e66094edbcb4d", false, false)
|
||||||
|
|
||||||
|
checkLatestReleaseAndCount(t, session, "/user2/repo1", "v0.0.1", translation.NewLocale("en-US").Tr("repo.release.stable"), 4)
|
||||||
|
}
|
||||||
|
|
|
@ -15,8 +15,8 @@ import (
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
)
|
)
|
||||||
|
|
||||||
func testRepoMigrate(t testing.TB, session *TestSession, cloneAddr, repoName string) *httptest.ResponseRecorder {
|
func testRepoMigrate(t testing.TB, session *TestSession, cloneAddr, repoName string, service structs.GitServiceType) *httptest.ResponseRecorder {
|
||||||
req := NewRequest(t, "GET", fmt.Sprintf("/repo/migrate?service_type=%d", structs.PlainGitService)) // render plain git migration page
|
req := NewRequest(t, "GET", fmt.Sprintf("/repo/migrate?service_type=%d", service)) // render plain git migration page
|
||||||
resp := session.MakeRequest(t, req, http.StatusOK)
|
resp := session.MakeRequest(t, req, http.StatusOK)
|
||||||
htmlDoc := NewHTMLParser(t, resp.Body)
|
htmlDoc := NewHTMLParser(t, resp.Body)
|
||||||
|
|
||||||
|
@ -31,7 +31,7 @@ func testRepoMigrate(t testing.TB, session *TestSession, cloneAddr, repoName str
|
||||||
"clone_addr": cloneAddr,
|
"clone_addr": cloneAddr,
|
||||||
"uid": uid,
|
"uid": uid,
|
||||||
"repo_name": repoName,
|
"repo_name": repoName,
|
||||||
"service": fmt.Sprintf("%d", structs.PlainGitService),
|
"service": fmt.Sprintf("%d", service),
|
||||||
})
|
})
|
||||||
resp = session.MakeRequest(t, req, http.StatusSeeOther)
|
resp = session.MakeRequest(t, req, http.StatusSeeOther)
|
||||||
|
|
||||||
|
@ -41,5 +41,17 @@ func testRepoMigrate(t testing.TB, session *TestSession, cloneAddr, repoName str
|
||||||
func TestRepoMigrate(t *testing.T) {
|
func TestRepoMigrate(t *testing.T) {
|
||||||
defer tests.PrepareTestEnv(t)()
|
defer tests.PrepareTestEnv(t)()
|
||||||
session := loginUser(t, "user2")
|
session := loginUser(t, "user2")
|
||||||
testRepoMigrate(t, session, "https://github.com/go-gitea/test_repo.git", "git")
|
for _, s := range []struct {
|
||||||
|
testName string
|
||||||
|
cloneAddr string
|
||||||
|
repoName string
|
||||||
|
service structs.GitServiceType
|
||||||
|
}{
|
||||||
|
{"TestMigrateGithub", "https://github.com/go-gitea/test_repo.git", "git", structs.PlainGitService},
|
||||||
|
{"TestMigrateGithub", "https://github.com/go-gitea/test_repo.git", "github", structs.GithubService},
|
||||||
|
} {
|
||||||
|
t.Run(s.testName, func(t *testing.T) {
|
||||||
|
testRepoMigrate(t, session, s.cloneAddr, s.repoName, s.service)
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -444,3 +444,107 @@ func TestGeneratedSourceLink(t *testing.T) {
|
||||||
assert.Equal(t, "/user27/repo49/src/commit/aacbdfe9e1c4b47f60abe81849045fa4e96f1d75/test/test.txt", dataURL)
|
assert.Equal(t, "/user27/repo49/src/commit/aacbdfe9e1c4b47f60abe81849045fa4e96f1d75/test/test.txt", dataURL)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestRepoHTMLTitle(t *testing.T) {
|
||||||
|
defer tests.PrepareTestEnv(t)()
|
||||||
|
|
||||||
|
t.Run("Repository homepage", func(t *testing.T) {
|
||||||
|
t.Run("Without description", func(t *testing.T) {
|
||||||
|
defer tests.PrintCurrentTest(t)()
|
||||||
|
|
||||||
|
htmlTitle := GetHTMLTitle(t, nil, "/user2/repo1")
|
||||||
|
assert.EqualValues(t, "user2/repo1 - Gitea: Git with a cup of tea", htmlTitle)
|
||||||
|
})
|
||||||
|
t.Run("With description", func(t *testing.T) {
|
||||||
|
defer tests.PrintCurrentTest(t)()
|
||||||
|
|
||||||
|
htmlTitle := GetHTMLTitle(t, nil, "/user27/repo49")
|
||||||
|
assert.EqualValues(t, "user27/repo49: A wonderful repository with more than just a README.md - Gitea: Git with a cup of tea", htmlTitle)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Code view", func(t *testing.T) {
|
||||||
|
t.Run("Directory", func(t *testing.T) {
|
||||||
|
t.Run("Default branch", func(t *testing.T) {
|
||||||
|
defer tests.PrintCurrentTest(t)()
|
||||||
|
|
||||||
|
htmlTitle := GetHTMLTitle(t, nil, "/user2/repo59/src/branch/master/deep/nesting")
|
||||||
|
assert.EqualValues(t, "repo59/deep/nesting at master - user2/repo59 - Gitea: Git with a cup of tea", htmlTitle)
|
||||||
|
})
|
||||||
|
t.Run("Non-default branch", func(t *testing.T) {
|
||||||
|
defer tests.PrintCurrentTest(t)()
|
||||||
|
|
||||||
|
htmlTitle := GetHTMLTitle(t, nil, "/user2/repo59/src/branch/cake-recipe/deep/nesting")
|
||||||
|
assert.EqualValues(t, "repo59/deep/nesting at cake-recipe - user2/repo59 - Gitea: Git with a cup of tea", htmlTitle)
|
||||||
|
})
|
||||||
|
t.Run("Commit", func(t *testing.T) {
|
||||||
|
defer tests.PrintCurrentTest(t)()
|
||||||
|
|
||||||
|
htmlTitle := GetHTMLTitle(t, nil, "/user2/repo59/src/commit/d8f53dfb33f6ccf4169c34970b5e747511c18beb/deep/nesting/")
|
||||||
|
assert.EqualValues(t, "repo59/deep/nesting at d8f53dfb33f6ccf4169c34970b5e747511c18beb - user2/repo59 - Gitea: Git with a cup of tea", htmlTitle)
|
||||||
|
})
|
||||||
|
t.Run("Tag", func(t *testing.T) {
|
||||||
|
defer tests.PrintCurrentTest(t)()
|
||||||
|
|
||||||
|
htmlTitle := GetHTMLTitle(t, nil, "/user2/repo59/src/tag/v1.0/deep/nesting/")
|
||||||
|
assert.EqualValues(t, "repo59/deep/nesting at v1.0 - user2/repo59 - Gitea: Git with a cup of tea", htmlTitle)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
t.Run("File", func(t *testing.T) {
|
||||||
|
t.Run("Default branch", func(t *testing.T) {
|
||||||
|
defer tests.PrintCurrentTest(t)()
|
||||||
|
|
||||||
|
htmlTitle := GetHTMLTitle(t, nil, "/user2/repo59/src/branch/master/deep/nesting/folder/secret_sauce_recipe.txt")
|
||||||
|
assert.EqualValues(t, "repo59/deep/nesting/folder/secret_sauce_recipe.txt at master - user2/repo59 - Gitea: Git with a cup of tea", htmlTitle)
|
||||||
|
})
|
||||||
|
t.Run("Non-default branch", func(t *testing.T) {
|
||||||
|
defer tests.PrintCurrentTest(t)()
|
||||||
|
|
||||||
|
htmlTitle := GetHTMLTitle(t, nil, "/user2/repo59/src/branch/cake-recipe/deep/nesting/folder/secret_sauce_recipe.txt")
|
||||||
|
assert.EqualValues(t, "repo59/deep/nesting/folder/secret_sauce_recipe.txt at cake-recipe - user2/repo59 - Gitea: Git with a cup of tea", htmlTitle)
|
||||||
|
})
|
||||||
|
t.Run("Commit", func(t *testing.T) {
|
||||||
|
defer tests.PrintCurrentTest(t)()
|
||||||
|
|
||||||
|
htmlTitle := GetHTMLTitle(t, nil, "/user2/repo59/src/commit/d8f53dfb33f6ccf4169c34970b5e747511c18beb/deep/nesting/folder/secret_sauce_recipe.txt")
|
||||||
|
assert.EqualValues(t, "repo59/deep/nesting/folder/secret_sauce_recipe.txt at d8f53dfb33f6ccf4169c34970b5e747511c18beb - user2/repo59 - Gitea: Git with a cup of tea", htmlTitle)
|
||||||
|
})
|
||||||
|
t.Run("Tag", func(t *testing.T) {
|
||||||
|
defer tests.PrintCurrentTest(t)()
|
||||||
|
|
||||||
|
htmlTitle := GetHTMLTitle(t, nil, "/user2/repo59/src/tag/v1.0/deep/nesting/folder/secret_sauce_recipe.txt")
|
||||||
|
assert.EqualValues(t, "repo59/deep/nesting/folder/secret_sauce_recipe.txt at v1.0 - user2/repo59 - Gitea: Git with a cup of tea", htmlTitle)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Issues view", func(t *testing.T) {
|
||||||
|
t.Run("Overview page", func(t *testing.T) {
|
||||||
|
defer tests.PrintCurrentTest(t)()
|
||||||
|
|
||||||
|
htmlTitle := GetHTMLTitle(t, nil, "/user2/repo1/issues")
|
||||||
|
assert.EqualValues(t, "Issues - user2/repo1 - Gitea: Git with a cup of tea", htmlTitle)
|
||||||
|
})
|
||||||
|
t.Run("View issue page", func(t *testing.T) {
|
||||||
|
defer tests.PrintCurrentTest(t)()
|
||||||
|
|
||||||
|
htmlTitle := GetHTMLTitle(t, nil, "/user2/repo1/issues/1")
|
||||||
|
assert.EqualValues(t, "#1 - issue1 - user2/repo1 - Gitea: Git with a cup of tea", htmlTitle)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Pull requests view", func(t *testing.T) {
|
||||||
|
t.Run("Overview page", func(t *testing.T) {
|
||||||
|
defer tests.PrintCurrentTest(t)()
|
||||||
|
|
||||||
|
htmlTitle := GetHTMLTitle(t, nil, "/user2/repo1/pulls")
|
||||||
|
assert.EqualValues(t, "Pull Requests - user2/repo1 - Gitea: Git with a cup of tea", htmlTitle)
|
||||||
|
})
|
||||||
|
t.Run("View pull request", func(t *testing.T) {
|
||||||
|
defer tests.PrintCurrentTest(t)()
|
||||||
|
|
||||||
|
htmlTitle := GetHTMLTitle(t, nil, "/user2/repo1/pulls/2")
|
||||||
|
assert.EqualValues(t, "#2 - issue2 - user2/repo1 - Gitea: Git with a cup of tea", htmlTitle)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
|
@ -17,10 +17,6 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 767.98px) {
|
@media (max-width: 767.98px) {
|
||||||
.issue-list-toolbar-right .dropdown .menu {
|
|
||||||
left: auto !important;
|
|
||||||
right: auto !important;
|
|
||||||
}
|
|
||||||
.issue-list-navbar {
|
.issue-list-navbar {
|
||||||
order: 0;
|
order: 0;
|
||||||
}
|
}
|
||||||
|
@ -31,6 +27,37 @@
|
||||||
.issue-list-search {
|
.issue-list-search {
|
||||||
order: 2 !important;
|
order: 2 !important;
|
||||||
}
|
}
|
||||||
|
/* Don't use flex wrap on mobile as it takes too much vertical space.
|
||||||
|
* Only set overflow properties on mobile screens, because while the
|
||||||
|
* CSS trick to pop out from overflowing works on desktop screen, it
|
||||||
|
* has a massive flaw that it cannot inherited any max width from it's 'real'
|
||||||
|
* parent and therefor ends up taking more vertical space than is desired.
|
||||||
|
**/
|
||||||
|
.issue-list-toolbar-right .filter.menu {
|
||||||
|
flex-wrap: nowrap;
|
||||||
|
overflow-x: auto;
|
||||||
|
overflow-y: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* The following few CSS was created with care and built with the information
|
||||||
|
* from CSS-Tricks: https://css-tricks.com/popping-hidden-overflow/
|
||||||
|
*/
|
||||||
|
|
||||||
|
/* It's important that every element up to .issue-list-toolbar-right doesn't
|
||||||
|
* have a position set, such that element that wants to pop out will use
|
||||||
|
* .issue-list-toolbar-right as 'clip parent' and thereby avoids the
|
||||||
|
* overflow-y: hidden.
|
||||||
|
*/
|
||||||
|
.issue-list-toolbar-right .filter.menu > .dropdown.item {
|
||||||
|
position: initial;
|
||||||
|
}
|
||||||
|
/* It's important that this element and not an child has `position` set.
|
||||||
|
* Set width so that overflow-x knows where to stop overflowing.
|
||||||
|
*/
|
||||||
|
.issue-list-toolbar-right {
|
||||||
|
position: relative;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#issue-list .flex-item-title .labels-list {
|
#issue-list .flex-item-title .labels-list {
|
||||||
|
|
Loading…
Reference in a new issue