mirror of
https://github.com/go-gitea/gitea.git
synced 2025-11-05 18:32:41 +09:00
Compare commits
23 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
97171be1b4 | ||
|
|
9ef2a338d8 | ||
|
|
66ccae8b90 | ||
|
|
0e6489317e | ||
|
|
67dc1ff926 | ||
|
|
4ee4c06b07 | ||
|
|
3063e37802 | ||
|
|
a40e15a116 | ||
|
|
8f75f61b64 | ||
|
|
25e409e025 | ||
|
|
f8f24d83cf | ||
|
|
15e93a751c | ||
|
|
5a9b3bfa50 | ||
|
|
dd901983c0 | ||
|
|
7f962a16c9 | ||
|
|
5d81f6d73f | ||
|
|
eee4a752a5 | ||
|
|
b3516767fb | ||
|
|
8d1be2a9c5 | ||
|
|
e46f9ff534 | ||
|
|
690e810bcc | ||
|
|
35983ac0a8 | ||
|
|
4b3400bd9c |
26
CHANGELOG.md
26
CHANGELOG.md
@@ -4,6 +4,32 @@ This changelog goes through the changes that have been made in each release
|
||||
without substantial changes to our git log; to see the highlights of what has
|
||||
been added to each release, please refer to the [blog](https://blog.gitea.com).
|
||||
|
||||
## [1.23.7](https://github.com/go-gitea/gitea/releases/tag/1.23.7) - 2025-04-07
|
||||
|
||||
* Enhancements
|
||||
* Add a config option to block "expensive" pages for anonymous users (#34024) (#34071)
|
||||
* Also check default ssh-cert location for host (#34099) (#34100) (#34116)
|
||||
* BUGFIXES
|
||||
* Fix discord webhook 400 status code when description limit is exceeded (#34084) (#34124)
|
||||
* Get changed files based on merge base when checking `pull_request` actions trigger (#34106) (#34120)
|
||||
* Fix invalid version in RPM package path (#34112) (#34115)
|
||||
* Return default avatar url when user id is zero rather than updating database (#34094) (#34095)
|
||||
* Add additional ReplaceAll in pathsep to cater for different pathsep (#34061) (#34070)
|
||||
* Try to fix check-attr bug (#34029) (#34033)
|
||||
* Git client will follow 301 but 307 (#34005) (#34010)
|
||||
* Fix block expensive for 1.23 (#34127)
|
||||
* Fix markdown frontmatter rendering (#34102) (#34107)
|
||||
* Add new CLI flags to set name and scopes when creating a user with access token (#34080) (#34103)
|
||||
* Do not show 500 error when default branch doesn't exist (#34096) (#34097)
|
||||
* Hide activity contributors, recent commits and code frequrency left tabs if there is no code permission (#34053) (#34065)
|
||||
* Simplify emoji rendering (#34048) (#34049)
|
||||
* Adjust the layout of the toolbar on the Issues/Projects page (#33667) (#34047)
|
||||
* Pull request updates will also trigger code owners review requests (#33744) (#34045)
|
||||
* Fix org repo creation being limited by user limits (#34030) (#34044)
|
||||
* Fix git client accessing renamed repo (#34034) (#34043)
|
||||
* Fix the issue with error message logging for the `check-attr` command on Windows OS. (#34035) (#34036)
|
||||
* Polyfill WeakRef (#34025) (#34028)
|
||||
|
||||
## [1.23.6](https://github.com/go-gitea/gitea/releases/tag/v1.23.6) - 2025-03-24
|
||||
|
||||
* SECURITY
|
||||
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
auth_model "code.gitea.io/gitea/models/auth"
|
||||
"code.gitea.io/gitea/models/db"
|
||||
@@ -61,6 +62,16 @@ var microcmdUserCreate = &cli.Command{
|
||||
Name: "access-token",
|
||||
Usage: "Generate access token for the user",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "access-token-name",
|
||||
Usage: `Name of the generated access token`,
|
||||
Value: "gitea-admin",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "access-token-scopes",
|
||||
Usage: `Scopes of the generated access token, comma separated. Examples: "all", "public-only,read:issue", "write:repository,write:user"`,
|
||||
Value: "all",
|
||||
},
|
||||
&cli.BoolFlag{
|
||||
Name: "restricted",
|
||||
Usage: "Make a restricted user account",
|
||||
@@ -162,23 +173,39 @@ func runCreateUser(c *cli.Context) error {
|
||||
IsRestricted: restricted,
|
||||
}
|
||||
|
||||
var accessTokenName string
|
||||
var accessTokenScope auth_model.AccessTokenScope
|
||||
if c.IsSet("access-token") {
|
||||
accessTokenName = strings.TrimSpace(c.String("access-token-name"))
|
||||
if accessTokenName == "" {
|
||||
return errors.New("access-token-name cannot be empty")
|
||||
}
|
||||
var err error
|
||||
accessTokenScope, err = auth_model.AccessTokenScope(c.String("access-token-scopes")).Normalize()
|
||||
if err != nil {
|
||||
return fmt.Errorf("invalid access token scope provided: %w", err)
|
||||
}
|
||||
if !accessTokenScope.HasPermissionScope() {
|
||||
return errors.New("access token does not have any permission")
|
||||
}
|
||||
} else if c.IsSet("access-token-name") || c.IsSet("access-token-scopes") {
|
||||
return errors.New("access-token-name and access-token-scopes flags are only valid when access-token flag is set")
|
||||
}
|
||||
|
||||
// arguments should be prepared before creating the user & access token, in case there is anything wrong
|
||||
|
||||
// create the user
|
||||
if err := user_model.CreateUser(ctx, u, &user_model.Meta{}, overwriteDefault); err != nil {
|
||||
return fmt.Errorf("CreateUser: %w", err)
|
||||
}
|
||||
|
||||
if c.Bool("access-token") {
|
||||
t := &auth_model.AccessToken{
|
||||
Name: "gitea-admin",
|
||||
UID: u.ID,
|
||||
}
|
||||
|
||||
// create the access token
|
||||
if accessTokenScope != "" {
|
||||
t := &auth_model.AccessToken{Name: accessTokenName, UID: u.ID, Scope: accessTokenScope}
|
||||
if err := auth_model.NewAccessToken(ctx, t); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fmt.Printf("Access token was successfully created... %s\n", t.Token)
|
||||
}
|
||||
|
||||
fmt.Printf("New user '%s' has been successfully created!\n", username)
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -8,37 +8,97 @@ import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
auth_model "code.gitea.io/gitea/models/auth"
|
||||
"code.gitea.io/gitea/models/db"
|
||||
"code.gitea.io/gitea/models/unittest"
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestAdminUserCreate(t *testing.T) {
|
||||
app := NewMainApp(AppVersion{})
|
||||
|
||||
reset := func() {
|
||||
assert.NoError(t, db.TruncateBeans(db.DefaultContext, &user_model.User{}))
|
||||
assert.NoError(t, db.TruncateBeans(db.DefaultContext, &user_model.EmailAddress{}))
|
||||
require.NoError(t, db.TruncateBeans(db.DefaultContext, &user_model.User{}))
|
||||
require.NoError(t, db.TruncateBeans(db.DefaultContext, &user_model.EmailAddress{}))
|
||||
require.NoError(t, db.TruncateBeans(db.DefaultContext, &auth_model.AccessToken{}))
|
||||
}
|
||||
t.Run("MustChangePassword", func(t *testing.T) {
|
||||
type check struct{ IsAdmin, MustChangePassword bool }
|
||||
createCheck := func(name, args string) check {
|
||||
assert.NoError(t, app.Run(strings.Fields(fmt.Sprintf("./gitea admin user create --username %s --email %s@gitea.local %s --password foobar", name, name, args))))
|
||||
u := unittest.AssertExistsAndLoadBean(t, &user_model.User{LowerName: name})
|
||||
return check{u.IsAdmin, u.MustChangePassword}
|
||||
}
|
||||
reset()
|
||||
assert.Equal(t, check{IsAdmin: false, MustChangePassword: false}, createCheck("u", ""), "first non-admin user doesn't need to change password")
|
||||
|
||||
reset()
|
||||
assert.Equal(t, check{IsAdmin: true, MustChangePassword: false}, createCheck("u", "--admin"), "first admin user doesn't need to change password")
|
||||
|
||||
reset()
|
||||
assert.Equal(t, check{IsAdmin: true, MustChangePassword: true}, createCheck("u", "--admin --must-change-password"))
|
||||
assert.Equal(t, check{IsAdmin: true, MustChangePassword: true}, createCheck("u2", "--admin"))
|
||||
assert.Equal(t, check{IsAdmin: true, MustChangePassword: false}, createCheck("u3", "--admin --must-change-password=false"))
|
||||
assert.Equal(t, check{IsAdmin: false, MustChangePassword: true}, createCheck("u4", ""))
|
||||
assert.Equal(t, check{IsAdmin: false, MustChangePassword: false}, createCheck("u5", "--must-change-password=false"))
|
||||
})
|
||||
|
||||
createUser := func(name, args string) error {
|
||||
return app.Run(strings.Fields(fmt.Sprintf("./gitea admin user create --username %s --email %s@gitea.local %s", name, name, args)))
|
||||
}
|
||||
|
||||
type createCheck struct{ IsAdmin, MustChangePassword bool }
|
||||
createUser := func(name, args string) createCheck {
|
||||
assert.NoError(t, app.Run(strings.Fields(fmt.Sprintf("./gitea admin user create --username %s --email %s@gitea.local %s --password foobar", name, name, args))))
|
||||
u := unittest.AssertExistsAndLoadBean(t, &user_model.User{LowerName: name})
|
||||
return createCheck{u.IsAdmin, u.MustChangePassword}
|
||||
}
|
||||
reset()
|
||||
assert.Equal(t, createCheck{IsAdmin: false, MustChangePassword: false}, createUser("u", ""), "first non-admin user doesn't need to change password")
|
||||
t.Run("AccessToken", func(t *testing.T) {
|
||||
// no generated access token
|
||||
reset()
|
||||
assert.NoError(t, createUser("u", "--random-password"))
|
||||
assert.Equal(t, 1, unittest.GetCount(t, &user_model.User{}))
|
||||
assert.Equal(t, 0, unittest.GetCount(t, &auth_model.AccessToken{}))
|
||||
|
||||
reset()
|
||||
assert.Equal(t, createCheck{IsAdmin: true, MustChangePassword: false}, createUser("u", "--admin"), "first admin user doesn't need to change password")
|
||||
// using "--access-token" only means "all" access
|
||||
reset()
|
||||
assert.NoError(t, createUser("u", "--random-password --access-token"))
|
||||
assert.Equal(t, 1, unittest.GetCount(t, &user_model.User{}))
|
||||
assert.Equal(t, 1, unittest.GetCount(t, &auth_model.AccessToken{}))
|
||||
accessToken := unittest.AssertExistsAndLoadBean(t, &auth_model.AccessToken{Name: "gitea-admin"})
|
||||
hasScopes, err := accessToken.Scope.HasScope(auth_model.AccessTokenScopeWriteAdmin, auth_model.AccessTokenScopeWriteRepository)
|
||||
assert.NoError(t, err)
|
||||
assert.True(t, hasScopes)
|
||||
|
||||
reset()
|
||||
assert.Equal(t, createCheck{IsAdmin: true, MustChangePassword: true}, createUser("u", "--admin --must-change-password"))
|
||||
assert.Equal(t, createCheck{IsAdmin: true, MustChangePassword: true}, createUser("u2", "--admin"))
|
||||
assert.Equal(t, createCheck{IsAdmin: true, MustChangePassword: false}, createUser("u3", "--admin --must-change-password=false"))
|
||||
assert.Equal(t, createCheck{IsAdmin: false, MustChangePassword: true}, createUser("u4", ""))
|
||||
assert.Equal(t, createCheck{IsAdmin: false, MustChangePassword: false}, createUser("u5", "--must-change-password=false"))
|
||||
// using "--access-token" with name & scopes
|
||||
reset()
|
||||
assert.NoError(t, createUser("u", "--random-password --access-token --access-token-name new-token-name --access-token-scopes read:issue,read:user"))
|
||||
assert.Equal(t, 1, unittest.GetCount(t, &user_model.User{}))
|
||||
assert.Equal(t, 1, unittest.GetCount(t, &auth_model.AccessToken{}))
|
||||
accessToken = unittest.AssertExistsAndLoadBean(t, &auth_model.AccessToken{Name: "new-token-name"})
|
||||
hasScopes, err = accessToken.Scope.HasScope(auth_model.AccessTokenScopeReadIssue, auth_model.AccessTokenScopeReadUser)
|
||||
assert.NoError(t, err)
|
||||
assert.True(t, hasScopes)
|
||||
hasScopes, err = accessToken.Scope.HasScope(auth_model.AccessTokenScopeWriteAdmin, auth_model.AccessTokenScopeWriteRepository)
|
||||
assert.NoError(t, err)
|
||||
assert.False(t, hasScopes)
|
||||
|
||||
// using "--access-token-name" without "--access-token"
|
||||
reset()
|
||||
err = createUser("u", "--random-password --access-token-name new-token-name")
|
||||
assert.Equal(t, 0, unittest.GetCount(t, &user_model.User{}))
|
||||
assert.Equal(t, 0, unittest.GetCount(t, &auth_model.AccessToken{}))
|
||||
assert.ErrorContains(t, err, "access-token-name and access-token-scopes flags are only valid when access-token flag is set")
|
||||
|
||||
// using "--access-token-scopes" without "--access-token"
|
||||
reset()
|
||||
err = createUser("u", "--random-password --access-token-scopes read:issue")
|
||||
assert.Equal(t, 0, unittest.GetCount(t, &user_model.User{}))
|
||||
assert.Equal(t, 0, unittest.GetCount(t, &auth_model.AccessToken{}))
|
||||
assert.ErrorContains(t, err, "access-token-name and access-token-scopes flags are only valid when access-token flag is set")
|
||||
|
||||
// empty permission
|
||||
reset()
|
||||
err = createUser("u", "--random-password --access-token --access-token-scopes public-only")
|
||||
assert.Equal(t, 0, unittest.GetCount(t, &user_model.User{}))
|
||||
assert.Equal(t, 0, unittest.GetCount(t, &auth_model.AccessToken{}))
|
||||
assert.ErrorContains(t, err, "access token does not have any permission")
|
||||
})
|
||||
}
|
||||
|
||||
@@ -34,8 +34,8 @@ var microcmdUserGenerateAccessToken = &cli.Command{
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "scopes",
|
||||
Value: "",
|
||||
Usage: "Comma separated list of scopes to apply to access token",
|
||||
Value: "all",
|
||||
Usage: `Comma separated list of scopes to apply to access token, examples: "all", "public-only,read:issue", "write:repository,write:user"`,
|
||||
},
|
||||
},
|
||||
Action: runGenerateAccessToken,
|
||||
@@ -43,7 +43,7 @@ var microcmdUserGenerateAccessToken = &cli.Command{
|
||||
|
||||
func runGenerateAccessToken(c *cli.Context) error {
|
||||
if !c.IsSet("username") {
|
||||
return errors.New("You must provide a username to generate a token for")
|
||||
return errors.New("you must provide a username to generate a token for")
|
||||
}
|
||||
|
||||
ctx, cancel := installSignals()
|
||||
@@ -77,6 +77,9 @@ func runGenerateAccessToken(c *cli.Context) error {
|
||||
if err != nil {
|
||||
return fmt.Errorf("invalid access token scope provided: %w", err)
|
||||
}
|
||||
if !accessTokenScope.HasPermissionScope() {
|
||||
return errors.New("access token does not have any permission")
|
||||
}
|
||||
t.Scope = accessTokenScope
|
||||
|
||||
// create the token
|
||||
|
||||
@@ -774,6 +774,9 @@ LEVEL = Info
|
||||
;ALLOW_ONLY_EXTERNAL_REGISTRATION = false
|
||||
;;
|
||||
;; User must sign in to view anything.
|
||||
;; After 1.23.7, it could be set to "expensive" to block anonymous users accessing some pages which consume a lot of resources,
|
||||
;; for example: block anonymous AI crawlers from accessing repo code pages.
|
||||
;; The "expensive" mode is experimental and subject to change.
|
||||
;REQUIRE_SIGNIN_VIEW = false
|
||||
;;
|
||||
;; Mail notification
|
||||
|
||||
@@ -31,6 +31,18 @@ if [ -e /data/ssh/ssh_host_ecdsa_cert ]; then
|
||||
SSH_ECDSA_CERT=${SSH_ECDSA_CERT:-"/data/ssh/ssh_host_ecdsa_cert"}
|
||||
fi
|
||||
|
||||
if [ -e /data/ssh/ssh_host_ed25519-cert.pub ]; then
|
||||
SSH_ED25519_CERT=${SSH_ED25519_CERT:-"/data/ssh/ssh_host_ed25519-cert.pub"}
|
||||
fi
|
||||
|
||||
if [ -e /data/ssh/ssh_host_rsa-cert.pub ]; then
|
||||
SSH_RSA_CERT=${SSH_RSA_CERT:-"/data/ssh/ssh_host_rsa-cert.pub"}
|
||||
fi
|
||||
|
||||
if [ -e /data/ssh/ssh_host_ecdsa-cert.pub ]; then
|
||||
SSH_ECDSA_CERT=${SSH_ECDSA_CERT:-"/data/ssh/ssh_host_ecdsa-cert.pub"}
|
||||
fi
|
||||
|
||||
if [ -d /etc/ssh ]; then
|
||||
SSH_PORT=${SSH_PORT:-"22"} \
|
||||
SSH_LISTEN_PORT=${SSH_LISTEN_PORT:-"${SSH_PORT}"} \
|
||||
|
||||
@@ -283,6 +283,10 @@ func (s AccessTokenScope) Normalize() (AccessTokenScope, error) {
|
||||
return bitmap.toScope(), nil
|
||||
}
|
||||
|
||||
func (s AccessTokenScope) HasPermissionScope() bool {
|
||||
return s != "" && s != AccessTokenScopePublicOnly
|
||||
}
|
||||
|
||||
// PublicOnly checks if this token scope is limited to public resources
|
||||
func (s AccessTokenScope) PublicOnly() (bool, error) {
|
||||
bitmap, err := s.parse()
|
||||
|
||||
@@ -173,6 +173,18 @@ func GetBranch(ctx context.Context, repoID int64, branchName string) (*Branch, e
|
||||
return &branch, nil
|
||||
}
|
||||
|
||||
// IsBranchExist returns true if the branch exists in the repository.
|
||||
func IsBranchExist(ctx context.Context, repoID int64, branchName string) (bool, error) {
|
||||
var branch Branch
|
||||
has, err := db.GetEngine(ctx).Where("repo_id=?", repoID).And("name=?", branchName).Get(&branch)
|
||||
if err != nil {
|
||||
return false, err
|
||||
} else if !has {
|
||||
return false, nil
|
||||
}
|
||||
return !branch.IsDeleted, nil
|
||||
}
|
||||
|
||||
func GetBranches(ctx context.Context, repoID int64, branchNames []string, includeDeleted bool) ([]*Branch, error) {
|
||||
branches := make([]*Branch, 0, len(branchNames))
|
||||
|
||||
|
||||
@@ -61,7 +61,9 @@ func GenerateRandomAvatar(ctx context.Context, u *User) error {
|
||||
|
||||
// AvatarLinkWithSize returns a link to the user's avatar with size. size <= 0 means default size
|
||||
func (u *User) AvatarLinkWithSize(ctx context.Context, size int) string {
|
||||
if u.IsGhost() || u.IsGiteaActions() {
|
||||
// ghost user was deleted, Gitea actions is a bot user, 0 means the user should be a virtual user
|
||||
// which comes from git configure information
|
||||
if u.IsGhost() || u.IsGiteaActions() || u.ID <= 0 {
|
||||
return avatars.DefaultAvatarLink()
|
||||
}
|
||||
|
||||
|
||||
@@ -463,7 +463,7 @@ func matchPullRequestEvent(gitRepo *git.Repository, commit *git.Commit, prPayloa
|
||||
matchTimes++
|
||||
}
|
||||
case "paths":
|
||||
filesChanged, err := headCommit.GetFilesChangedSinceCommit(prPayload.PullRequest.Base.Ref)
|
||||
filesChanged, err := headCommit.GetFilesChangedSinceCommit(prPayload.PullRequest.MergeBase)
|
||||
if err != nil {
|
||||
log.Error("GetFilesChangedSinceCommit [commit_sha1: %s]: %v", headCommit.ID.String(), err)
|
||||
} else {
|
||||
@@ -476,7 +476,7 @@ func matchPullRequestEvent(gitRepo *git.Repository, commit *git.Commit, prPayloa
|
||||
}
|
||||
}
|
||||
case "paths-ignore":
|
||||
filesChanged, err := headCommit.GetFilesChangedSinceCommit(prPayload.PullRequest.Base.Ref)
|
||||
filesChanged, err := headCommit.GetFilesChangedSinceCommit(prPayload.PullRequest.MergeBase)
|
||||
if err != nil {
|
||||
log.Error("GetFilesChangedSinceCommit [commit_sha1: %s]: %v", headCommit.ID.String(), err)
|
||||
} else {
|
||||
|
||||
@@ -350,9 +350,10 @@ func (c *Command) Run(opts *RunOpts) error {
|
||||
// We need to check if the context is canceled by the program on Windows.
|
||||
// This is because Windows does not have signal checking when terminating the process.
|
||||
// It always returns exit code 1, unlike Linux, which has many exit codes for signals.
|
||||
// `err.Error()` returns "exit status 1" when using the `git check-attr` command after the context is canceled.
|
||||
if runtime.GOOS == "windows" &&
|
||||
err != nil &&
|
||||
err.Error() == "" &&
|
||||
(err.Error() == "" || err.Error() == "exit status 1") &&
|
||||
cmd.ProcessState.ExitCode() == 1 &&
|
||||
ctx.Err() == context.Canceled {
|
||||
return ctx.Err()
|
||||
|
||||
@@ -280,7 +280,7 @@ func (wr *nulSeparatedAttributeWriter) Write(p []byte) (n int, err error) {
|
||||
}
|
||||
}
|
||||
wr.tmp = append(wr.tmp, p...)
|
||||
return len(p), nil
|
||||
return l, nil
|
||||
}
|
||||
|
||||
func (wr *nulSeparatedAttributeWriter) ReadAttribute() <-chan attributeTriple {
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
package markdown
|
||||
|
||||
import (
|
||||
"html/template"
|
||||
"strconv"
|
||||
|
||||
"github.com/yuin/goldmark/ast"
|
||||
@@ -29,9 +30,7 @@ func (n *Details) Kind() ast.NodeKind {
|
||||
|
||||
// NewDetails returns a new Paragraph node.
|
||||
func NewDetails() *Details {
|
||||
return &Details{
|
||||
BaseBlock: ast.BaseBlock{},
|
||||
}
|
||||
return &Details{}
|
||||
}
|
||||
|
||||
// Summary is a block that contains the summary of details block
|
||||
@@ -54,9 +53,7 @@ func (n *Summary) Kind() ast.NodeKind {
|
||||
|
||||
// NewSummary returns a new Summary node.
|
||||
func NewSummary() *Summary {
|
||||
return &Summary{
|
||||
BaseBlock: ast.BaseBlock{},
|
||||
}
|
||||
return &Summary{}
|
||||
}
|
||||
|
||||
// TaskCheckBoxListItem is a block that represents a list item of a markdown block with a checkbox
|
||||
@@ -95,29 +92,6 @@ type Icon struct {
|
||||
Name []byte
|
||||
}
|
||||
|
||||
// Dump implements Node.Dump .
|
||||
func (n *Icon) Dump(source []byte, level int) {
|
||||
m := map[string]string{}
|
||||
m["Name"] = string(n.Name)
|
||||
ast.DumpHelper(n, source, level, m, nil)
|
||||
}
|
||||
|
||||
// KindIcon is the NodeKind for Icon
|
||||
var KindIcon = ast.NewNodeKind("Icon")
|
||||
|
||||
// Kind implements Node.Kind.
|
||||
func (n *Icon) Kind() ast.NodeKind {
|
||||
return KindIcon
|
||||
}
|
||||
|
||||
// NewIcon returns a new Paragraph node.
|
||||
func NewIcon(name string) *Icon {
|
||||
return &Icon{
|
||||
BaseInline: ast.BaseInline{},
|
||||
Name: []byte(name),
|
||||
}
|
||||
}
|
||||
|
||||
// ColorPreview is an inline for a color preview
|
||||
type ColorPreview struct {
|
||||
ast.BaseInline
|
||||
@@ -175,3 +149,24 @@ func NewAttention(attentionType string) *Attention {
|
||||
AttentionType: attentionType,
|
||||
}
|
||||
}
|
||||
|
||||
var KindRawHTML = ast.NewNodeKind("RawHTML")
|
||||
|
||||
type RawHTML struct {
|
||||
ast.BaseBlock
|
||||
rawHTML template.HTML
|
||||
}
|
||||
|
||||
func (n *RawHTML) Dump(source []byte, level int) {
|
||||
m := map[string]string{}
|
||||
m["RawHTML"] = string(n.rawHTML)
|
||||
ast.DumpHelper(n, source, level, m, nil)
|
||||
}
|
||||
|
||||
func (n *RawHTML) Kind() ast.NodeKind {
|
||||
return KindRawHTML
|
||||
}
|
||||
|
||||
func NewRawHTML(rawHTML template.HTML) *RawHTML {
|
||||
return &RawHTML{rawHTML: rawHTML}
|
||||
}
|
||||
|
||||
@@ -4,23 +4,22 @@
|
||||
package markdown
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"code.gitea.io/gitea/modules/htmlutil"
|
||||
"code.gitea.io/gitea/modules/svg"
|
||||
|
||||
"github.com/yuin/goldmark/ast"
|
||||
east "github.com/yuin/goldmark/extension/ast"
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
func nodeToTable(meta *yaml.Node) ast.Node {
|
||||
for {
|
||||
if meta == nil {
|
||||
return nil
|
||||
}
|
||||
switch meta.Kind {
|
||||
case yaml.DocumentNode:
|
||||
meta = meta.Content[0]
|
||||
continue
|
||||
default:
|
||||
}
|
||||
break
|
||||
for meta != nil && meta.Kind == yaml.DocumentNode {
|
||||
meta = meta.Content[0]
|
||||
}
|
||||
if meta == nil {
|
||||
return nil
|
||||
}
|
||||
switch meta.Kind {
|
||||
case yaml.MappingNode:
|
||||
@@ -72,12 +71,28 @@ func sequenceNodeToTable(meta *yaml.Node) ast.Node {
|
||||
return table
|
||||
}
|
||||
|
||||
func nodeToDetails(meta *yaml.Node, icon string) ast.Node {
|
||||
func nodeToDetails(g *ASTTransformer, meta *yaml.Node) ast.Node {
|
||||
for meta != nil && meta.Kind == yaml.DocumentNode {
|
||||
meta = meta.Content[0]
|
||||
}
|
||||
if meta == nil {
|
||||
return nil
|
||||
}
|
||||
if meta.Kind != yaml.MappingNode {
|
||||
return nil
|
||||
}
|
||||
var keys []string
|
||||
for i := 0; i < len(meta.Content); i += 2 {
|
||||
if meta.Content[i].Kind == yaml.ScalarNode {
|
||||
keys = append(keys, meta.Content[i].Value)
|
||||
}
|
||||
}
|
||||
details := NewDetails()
|
||||
details.SetAttributeString(g.renderInternal.SafeAttr("class"), g.renderInternal.SafeValue("frontmatter-content"))
|
||||
summary := NewSummary()
|
||||
summary.AppendChild(summary, NewIcon(icon))
|
||||
summaryInnerHTML := htmlutil.HTMLFormat("%s %s", svg.RenderHTML("octicon-table", 12), strings.Join(keys, ", "))
|
||||
summary.AppendChild(summary, NewRawHTML(summaryInnerHTML))
|
||||
details.AppendChild(details, summary)
|
||||
details.AppendChild(details, nodeToTable(meta))
|
||||
|
||||
return details
|
||||
}
|
||||
|
||||
@@ -5,9 +5,6 @@ package markdown
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"code.gitea.io/gitea/modules/container"
|
||||
"code.gitea.io/gitea/modules/markup"
|
||||
@@ -51,7 +48,7 @@ func (g *ASTTransformer) Transform(node *ast.Document, reader text.Reader, pc pa
|
||||
|
||||
tocList := make([]Header, 0, 20)
|
||||
if rc.yamlNode != nil {
|
||||
metaNode := rc.toMetaNode()
|
||||
metaNode := rc.toMetaNode(g)
|
||||
if metaNode != nil {
|
||||
node.InsertBefore(node, firstChild, metaNode)
|
||||
}
|
||||
@@ -111,11 +108,6 @@ func (g *ASTTransformer) Transform(node *ast.Document, reader text.Reader, pc pa
|
||||
}
|
||||
}
|
||||
|
||||
// it is copied from old code, which is quite doubtful whether it is correct
|
||||
var reValidIconName = sync.OnceValue(func() *regexp.Regexp {
|
||||
return regexp.MustCompile(`^[-\w]+$`) // old: regexp.MustCompile("^[a-z ]+$")
|
||||
})
|
||||
|
||||
// NewHTMLRenderer creates a HTMLRenderer to render in the gitea form.
|
||||
func NewHTMLRenderer(renderInternal *internal.RenderInternal, opts ...html.Option) renderer.NodeRenderer {
|
||||
r := &HTMLRenderer{
|
||||
@@ -140,11 +132,11 @@ func (r *HTMLRenderer) RegisterFuncs(reg renderer.NodeRendererFuncRegisterer) {
|
||||
reg.Register(ast.KindDocument, r.renderDocument)
|
||||
reg.Register(KindDetails, r.renderDetails)
|
||||
reg.Register(KindSummary, r.renderSummary)
|
||||
reg.Register(KindIcon, r.renderIcon)
|
||||
reg.Register(ast.KindCodeSpan, r.renderCodeSpan)
|
||||
reg.Register(KindAttention, r.renderAttention)
|
||||
reg.Register(KindTaskCheckBoxListItem, r.renderTaskCheckBoxListItem)
|
||||
reg.Register(east.KindTaskCheckBox, r.renderTaskCheckBox)
|
||||
reg.Register(KindRawHTML, r.renderRawHTML)
|
||||
}
|
||||
|
||||
func (r *HTMLRenderer) renderDocument(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) {
|
||||
@@ -206,30 +198,14 @@ func (r *HTMLRenderer) renderSummary(w util.BufWriter, source []byte, node ast.N
|
||||
return ast.WalkContinue, nil
|
||||
}
|
||||
|
||||
func (r *HTMLRenderer) renderIcon(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) {
|
||||
func (r *HTMLRenderer) renderRawHTML(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) {
|
||||
if !entering {
|
||||
return ast.WalkContinue, nil
|
||||
}
|
||||
|
||||
n := node.(*Icon)
|
||||
|
||||
name := strings.TrimSpace(strings.ToLower(string(n.Name)))
|
||||
|
||||
if len(name) == 0 {
|
||||
// skip this
|
||||
return ast.WalkContinue, nil
|
||||
}
|
||||
|
||||
if !reValidIconName().MatchString(name) {
|
||||
// skip this
|
||||
return ast.WalkContinue, nil
|
||||
}
|
||||
|
||||
// FIXME: the "icon xxx" is from Fomantic UI, it's really questionable whether it still works correctly
|
||||
err := r.renderInternal.FormatWithSafeAttrs(w, `<i class="icon %s"></i>`, name)
|
||||
n := node.(*RawHTML)
|
||||
_, err := w.WriteString(string(r.renderInternal.ProtectSafeAttrs(n.rawHTML)))
|
||||
if err != nil {
|
||||
return ast.WalkStop, err
|
||||
}
|
||||
|
||||
return ast.WalkContinue, nil
|
||||
}
|
||||
|
||||
@@ -184,11 +184,7 @@ func render(ctx *markup.RenderContext, input io.Reader, output io.Writer) error
|
||||
// Preserve original length.
|
||||
bufWithMetadataLength := len(buf)
|
||||
|
||||
rc := &RenderConfig{
|
||||
Meta: markup.RenderMetaAsDetails,
|
||||
Icon: "table",
|
||||
Lang: "",
|
||||
}
|
||||
rc := &RenderConfig{Meta: markup.RenderMetaAsDetails}
|
||||
buf, _ = ExtractMetadataBytes(buf, rc)
|
||||
|
||||
metaLength := bufWithMetadataLength - len(buf)
|
||||
|
||||
@@ -383,18 +383,74 @@ func TestColorPreview(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestTaskList(t *testing.T) {
|
||||
func TestMarkdownFrontmatter(t *testing.T) {
|
||||
testcases := []struct {
|
||||
testcase string
|
||||
name string
|
||||
input string
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
"MapInFrontmatter",
|
||||
`---
|
||||
key1: val1
|
||||
key2: val2
|
||||
---
|
||||
test
|
||||
`,
|
||||
`<details class="frontmatter-content"><summary><span>octicon-table(12/)</span> key1, key2</summary><table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>key1</th>
|
||||
<th>key2</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>val1</td>
|
||||
<td>val2</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</details><p>test</p>
|
||||
`,
|
||||
},
|
||||
|
||||
{
|
||||
"ListInFrontmatter",
|
||||
`---
|
||||
- item1
|
||||
- item2
|
||||
---
|
||||
test
|
||||
`,
|
||||
`- item1
|
||||
- item2
|
||||
|
||||
<p>test</p>
|
||||
`,
|
||||
},
|
||||
|
||||
{
|
||||
"StringInFrontmatter",
|
||||
`---
|
||||
anything
|
||||
---
|
||||
test
|
||||
`,
|
||||
`anything
|
||||
|
||||
<p>test</p>
|
||||
`,
|
||||
},
|
||||
|
||||
{
|
||||
// data-source-position should take into account YAML frontmatter.
|
||||
"ListAfterFrontmatter",
|
||||
`---
|
||||
foo: bar
|
||||
---
|
||||
- [ ] task 1`,
|
||||
`<details><summary><i class="icon table"></i></summary><table>
|
||||
`<details class="frontmatter-content"><summary><span>octicon-table(12/)</span> foo</summary><table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>foo</th>
|
||||
@@ -414,9 +470,9 @@ foo: bar
|
||||
}
|
||||
|
||||
for _, test := range testcases {
|
||||
res, err := markdown.RenderString(markup.NewTestRenderContext(), test.testcase)
|
||||
assert.NoError(t, err, "Unexpected error in testcase: %q", test.testcase)
|
||||
assert.Equal(t, template.HTML(test.expected), res, "Unexpected result in testcase %q", test.testcase)
|
||||
res, err := markdown.RenderString(markup.NewTestRenderContext(), test.input)
|
||||
assert.NoError(t, err, "Unexpected error in testcase: %q", test.name)
|
||||
assert.Equal(t, test.expected, string(res), "Unexpected result in testcase %q", test.name)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -16,7 +16,6 @@ import (
|
||||
// RenderConfig represents rendering configuration for this file
|
||||
type RenderConfig struct {
|
||||
Meta markup.RenderMetaMode
|
||||
Icon string
|
||||
TOC string // "false": hide, "side"/empty: in sidebar, "main"/"true": in main view
|
||||
Lang string
|
||||
yamlNode *yaml.Node
|
||||
@@ -74,7 +73,7 @@ func (rc *RenderConfig) UnmarshalYAML(value *yaml.Node) error {
|
||||
|
||||
type yamlRenderConfig struct {
|
||||
Meta *string `yaml:"meta"`
|
||||
Icon *string `yaml:"details_icon"`
|
||||
Icon *string `yaml:"details_icon"` // deprecated, because there is no font icon, so no custom icon
|
||||
TOC *string `yaml:"include_toc"`
|
||||
Lang *string `yaml:"lang"`
|
||||
}
|
||||
@@ -96,10 +95,6 @@ func (rc *RenderConfig) UnmarshalYAML(value *yaml.Node) error {
|
||||
rc.Meta = renderMetaModeFromString(*cfg.Gitea.Meta)
|
||||
}
|
||||
|
||||
if cfg.Gitea.Icon != nil {
|
||||
rc.Icon = strings.TrimSpace(strings.ToLower(*cfg.Gitea.Icon))
|
||||
}
|
||||
|
||||
if cfg.Gitea.Lang != nil && *cfg.Gitea.Lang != "" {
|
||||
rc.Lang = *cfg.Gitea.Lang
|
||||
}
|
||||
@@ -111,7 +106,7 @@ func (rc *RenderConfig) UnmarshalYAML(value *yaml.Node) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (rc *RenderConfig) toMetaNode() ast.Node {
|
||||
func (rc *RenderConfig) toMetaNode(g *ASTTransformer) ast.Node {
|
||||
if rc.yamlNode == nil {
|
||||
return nil
|
||||
}
|
||||
@@ -119,7 +114,7 @@ func (rc *RenderConfig) toMetaNode() ast.Node {
|
||||
case markup.RenderMetaAsTable:
|
||||
return nodeToTable(rc.yamlNode)
|
||||
case markup.RenderMetaAsDetails:
|
||||
return nodeToDetails(rc.yamlNode, rc.Icon)
|
||||
return nodeToDetails(g, rc.yamlNode)
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
@@ -19,42 +20,36 @@ func TestRenderConfig_UnmarshalYAML(t *testing.T) {
|
||||
{
|
||||
"empty", &RenderConfig{
|
||||
Meta: "table",
|
||||
Icon: "table",
|
||||
Lang: "",
|
||||
}, "",
|
||||
},
|
||||
{
|
||||
"lang", &RenderConfig{
|
||||
Meta: "table",
|
||||
Icon: "table",
|
||||
Lang: "test",
|
||||
}, "lang: test",
|
||||
},
|
||||
{
|
||||
"metatable", &RenderConfig{
|
||||
Meta: "table",
|
||||
Icon: "table",
|
||||
Lang: "",
|
||||
}, "gitea: table",
|
||||
},
|
||||
{
|
||||
"metanone", &RenderConfig{
|
||||
Meta: "none",
|
||||
Icon: "table",
|
||||
Lang: "",
|
||||
}, "gitea: none",
|
||||
},
|
||||
{
|
||||
"metadetails", &RenderConfig{
|
||||
Meta: "details",
|
||||
Icon: "table",
|
||||
Lang: "",
|
||||
}, "gitea: details",
|
||||
},
|
||||
{
|
||||
"metawrong", &RenderConfig{
|
||||
Meta: "details",
|
||||
Icon: "table",
|
||||
Lang: "",
|
||||
}, "gitea: wrong",
|
||||
},
|
||||
@@ -62,7 +57,6 @@ func TestRenderConfig_UnmarshalYAML(t *testing.T) {
|
||||
"toc", &RenderConfig{
|
||||
TOC: "true",
|
||||
Meta: "table",
|
||||
Icon: "table",
|
||||
Lang: "",
|
||||
}, "include_toc: true",
|
||||
},
|
||||
@@ -70,14 +64,12 @@ func TestRenderConfig_UnmarshalYAML(t *testing.T) {
|
||||
"tocfalse", &RenderConfig{
|
||||
TOC: "false",
|
||||
Meta: "table",
|
||||
Icon: "table",
|
||||
Lang: "",
|
||||
}, "include_toc: false",
|
||||
},
|
||||
{
|
||||
"toclang", &RenderConfig{
|
||||
Meta: "table",
|
||||
Icon: "table",
|
||||
TOC: "true",
|
||||
Lang: "testlang",
|
||||
}, `
|
||||
@@ -88,7 +80,6 @@ func TestRenderConfig_UnmarshalYAML(t *testing.T) {
|
||||
{
|
||||
"complexlang", &RenderConfig{
|
||||
Meta: "table",
|
||||
Icon: "table",
|
||||
Lang: "testlang",
|
||||
}, `
|
||||
gitea:
|
||||
@@ -98,7 +89,6 @@ func TestRenderConfig_UnmarshalYAML(t *testing.T) {
|
||||
{
|
||||
"complexlang2", &RenderConfig{
|
||||
Meta: "table",
|
||||
Icon: "table",
|
||||
Lang: "testlang",
|
||||
}, `
|
||||
lang: notright
|
||||
@@ -109,7 +99,6 @@ func TestRenderConfig_UnmarshalYAML(t *testing.T) {
|
||||
{
|
||||
"complexlang", &RenderConfig{
|
||||
Meta: "table",
|
||||
Icon: "table",
|
||||
Lang: "testlang",
|
||||
}, `
|
||||
gitea:
|
||||
@@ -121,7 +110,6 @@ func TestRenderConfig_UnmarshalYAML(t *testing.T) {
|
||||
Lang: "two",
|
||||
Meta: "table",
|
||||
TOC: "true",
|
||||
Icon: "smiley",
|
||||
}, `
|
||||
lang: one
|
||||
include_toc: true
|
||||
@@ -137,7 +125,6 @@ func TestRenderConfig_UnmarshalYAML(t *testing.T) {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got := &RenderConfig{
|
||||
Meta: "table",
|
||||
Icon: "table",
|
||||
Lang: "",
|
||||
}
|
||||
if err := yaml.Unmarshal([]byte(strings.ReplaceAll(tt.args, "\t", " ")), got); err != nil {
|
||||
@@ -145,18 +132,9 @@ func TestRenderConfig_UnmarshalYAML(t *testing.T) {
|
||||
return
|
||||
}
|
||||
|
||||
if got.Meta != tt.expected.Meta {
|
||||
t.Errorf("Meta Expected %s Got %s", tt.expected.Meta, got.Meta)
|
||||
}
|
||||
if got.Icon != tt.expected.Icon {
|
||||
t.Errorf("Icon Expected %s Got %s", tt.expected.Icon, got.Icon)
|
||||
}
|
||||
if got.Lang != tt.expected.Lang {
|
||||
t.Errorf("Lang Expected %s Got %s", tt.expected.Lang, got.Lang)
|
||||
}
|
||||
if got.TOC != tt.expected.TOC {
|
||||
t.Errorf("TOC Expected %q Got %q", tt.expected.TOC, got.TOC)
|
||||
}
|
||||
assert.Equal(t, tt.expected.Meta, got.Meta)
|
||||
assert.Equal(t, tt.expected.Lang, got.Lang)
|
||||
assert.Equal(t, tt.expected.TOC, got.TOC)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,6 +26,7 @@ type ConfigKey interface {
|
||||
In(defaultVal string, candidates []string) string
|
||||
String() string
|
||||
Strings(delim string) []string
|
||||
Bool() (bool, error)
|
||||
|
||||
MustString(defaultVal string) string
|
||||
MustBool(defaultVal ...bool) bool
|
||||
|
||||
@@ -43,7 +43,8 @@ var Service = struct {
|
||||
ShowRegistrationButton bool
|
||||
EnablePasswordSignInForm bool
|
||||
ShowMilestonesDashboardPage bool
|
||||
RequireSignInView bool
|
||||
RequireSignInViewStrict bool
|
||||
BlockAnonymousAccessExpensive bool
|
||||
EnableNotifyMail bool
|
||||
EnableBasicAuth bool
|
||||
EnablePasskeyAuth bool
|
||||
@@ -159,7 +160,18 @@ func loadServiceFrom(rootCfg ConfigProvider) {
|
||||
Service.EmailDomainBlockList = CompileEmailGlobList(sec, "EMAIL_DOMAIN_BLOCKLIST")
|
||||
Service.ShowRegistrationButton = sec.Key("SHOW_REGISTRATION_BUTTON").MustBool(!(Service.DisableRegistration || Service.AllowOnlyExternalRegistration))
|
||||
Service.ShowMilestonesDashboardPage = sec.Key("SHOW_MILESTONES_DASHBOARD_PAGE").MustBool(true)
|
||||
Service.RequireSignInView = sec.Key("REQUIRE_SIGNIN_VIEW").MustBool()
|
||||
|
||||
// boolean values are considered as "strict"
|
||||
var err error
|
||||
Service.RequireSignInViewStrict, err = sec.Key("REQUIRE_SIGNIN_VIEW").Bool()
|
||||
if s := sec.Key("REQUIRE_SIGNIN_VIEW").String(); err != nil && s != "" {
|
||||
// non-boolean value only supports "expensive" at the moment
|
||||
Service.BlockAnonymousAccessExpensive = s == "expensive"
|
||||
if !Service.BlockAnonymousAccessExpensive {
|
||||
log.Error("Invalid config option: REQUIRE_SIGNIN_VIEW = %s", s)
|
||||
}
|
||||
}
|
||||
|
||||
Service.EnableBasicAuth = sec.Key("ENABLE_BASIC_AUTHENTICATION").MustBool(true)
|
||||
Service.EnablePasswordSignInForm = sec.Key("ENABLE_PASSWORD_SIGNIN_FORM").MustBool(true)
|
||||
Service.EnablePasskeyAuth = sec.Key("ENABLE_PASSKEY_AUTHENTICATION").MustBool(true)
|
||||
|
||||
@@ -7,16 +7,14 @@ import (
|
||||
"testing"
|
||||
|
||||
"code.gitea.io/gitea/modules/structs"
|
||||
"code.gitea.io/gitea/modules/test"
|
||||
|
||||
"github.com/gobwas/glob"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestLoadServices(t *testing.T) {
|
||||
oldService := Service
|
||||
defer func() {
|
||||
Service = oldService
|
||||
}()
|
||||
defer test.MockVariableValue(&Service)()
|
||||
|
||||
cfg, err := NewConfigProviderFromData(`
|
||||
[service]
|
||||
@@ -48,10 +46,7 @@ EMAIL_DOMAIN_BLOCKLIST = d3, *.b
|
||||
}
|
||||
|
||||
func TestLoadServiceVisibilityModes(t *testing.T) {
|
||||
oldService := Service
|
||||
defer func() {
|
||||
Service = oldService
|
||||
}()
|
||||
defer test.MockVariableValue(&Service)()
|
||||
|
||||
kases := map[string]func(){
|
||||
`
|
||||
@@ -130,3 +125,33 @@ ALLOWED_USER_VISIBILITY_MODES = public, limit, privated
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadServiceRequireSignInView(t *testing.T) {
|
||||
defer test.MockVariableValue(&Service)()
|
||||
|
||||
cfg, err := NewConfigProviderFromData(`
|
||||
[service]
|
||||
`)
|
||||
assert.NoError(t, err)
|
||||
loadServiceFrom(cfg)
|
||||
assert.False(t, Service.RequireSignInViewStrict)
|
||||
assert.False(t, Service.BlockAnonymousAccessExpensive)
|
||||
|
||||
cfg, err = NewConfigProviderFromData(`
|
||||
[service]
|
||||
REQUIRE_SIGNIN_VIEW = true
|
||||
`)
|
||||
assert.NoError(t, err)
|
||||
loadServiceFrom(cfg)
|
||||
assert.True(t, Service.RequireSignInViewStrict)
|
||||
assert.False(t, Service.BlockAnonymousAccessExpensive)
|
||||
|
||||
cfg, err = NewConfigProviderFromData(`
|
||||
[service]
|
||||
REQUIRE_SIGNIN_VIEW = expensive
|
||||
`)
|
||||
assert.NoError(t, err)
|
||||
loadServiceFrom(cfg)
|
||||
assert.False(t, Service.RequireSignInViewStrict)
|
||||
assert.True(t, Service.BlockAnonymousAccessExpensive)
|
||||
}
|
||||
|
||||
@@ -2699,6 +2699,7 @@ branch.restore_success = Branch "%s" has been restored.
|
||||
branch.restore_failed = Failed to restore branch "%s".
|
||||
branch.protected_deletion_failed = Branch "%s" is protected. It cannot be deleted.
|
||||
branch.default_deletion_failed = Branch "%s" is the default branch. It cannot be deleted.
|
||||
branch.default_branch_not_exist = Default branch "%s" does not exist.
|
||||
branch.restore = Restore Branch "%s"
|
||||
branch.download = Download Branch "%s"
|
||||
branch.rename = Rename Branch "%s"
|
||||
|
||||
@@ -51,7 +51,7 @@ func apiError(ctx *context.Context, status int, obj any) {
|
||||
|
||||
// https://rust-lang.github.io/rfcs/2789-sparse-index.html
|
||||
func RepositoryConfig(ctx *context.Context) {
|
||||
ctx.JSON(http.StatusOK, cargo_service.BuildConfig(ctx.Package.Owner, setting.Service.RequireSignInView || ctx.Package.Owner.Visibility != structs.VisibleTypePublic))
|
||||
ctx.JSON(http.StatusOK, cargo_service.BuildConfig(ctx.Package.Owner, setting.Service.RequireSignInViewStrict || ctx.Package.Owner.Visibility != structs.VisibleTypePublic))
|
||||
}
|
||||
|
||||
func EnumeratePackageVersions(ctx *context.Context) {
|
||||
|
||||
@@ -126,7 +126,7 @@ func apiUnauthorizedError(ctx *context.Context) {
|
||||
|
||||
// ReqContainerAccess is a middleware which checks the current user valid (real user or ghost if anonymous access is enabled)
|
||||
func ReqContainerAccess(ctx *context.Context) {
|
||||
if ctx.Doer == nil || (setting.Service.RequireSignInView && ctx.Doer.IsGhost()) {
|
||||
if ctx.Doer == nil || (setting.Service.RequireSignInViewStrict && ctx.Doer.IsGhost()) {
|
||||
apiUnauthorizedError(ctx)
|
||||
}
|
||||
}
|
||||
@@ -152,7 +152,7 @@ func Authenticate(ctx *context.Context) {
|
||||
u := ctx.Doer
|
||||
packageScope := auth_service.GetAccessScope(ctx.Data)
|
||||
if u == nil {
|
||||
if setting.Service.RequireSignInView {
|
||||
if setting.Service.RequireSignInViewStrict {
|
||||
apiUnauthorizedError(ctx)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -356,7 +356,7 @@ func reqToken() func(ctx *context.APIContext) {
|
||||
|
||||
func reqExploreSignIn() func(ctx *context.APIContext) {
|
||||
return func(ctx *context.APIContext) {
|
||||
if (setting.Service.RequireSignInView || setting.Service.Explore.RequireSigninView) && !ctx.IsSigned {
|
||||
if (setting.Service.RequireSignInViewStrict || setting.Service.Explore.RequireSigninView) && !ctx.IsSigned {
|
||||
ctx.Error(http.StatusUnauthorized, "reqExploreSignIn", "you must be signed in to search for users")
|
||||
}
|
||||
}
|
||||
@@ -874,7 +874,7 @@ func Routes() *web.Router {
|
||||
m.Use(apiAuth(buildAuthGroup()))
|
||||
|
||||
m.Use(verifyAuthWithOptions(&common.VerifyOptions{
|
||||
SignInRequired: setting.Service.RequireSignInView,
|
||||
SignInRequired: setting.Service.RequireSignInViewStrict,
|
||||
}))
|
||||
|
||||
addActionsRoutes := func(
|
||||
|
||||
92
routers/common/blockexpensive.go
Normal file
92
routers/common/blockexpensive.go
Normal file
@@ -0,0 +1,92 @@
|
||||
// Copyright 2025 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package common
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/modules/web/middleware"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
)
|
||||
|
||||
func BlockExpensive() func(next http.Handler) http.Handler {
|
||||
if !setting.Service.BlockAnonymousAccessExpensive {
|
||||
return nil
|
||||
}
|
||||
return func(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
|
||||
ret := determineRequestPriority(req.Context())
|
||||
if !ret.SignedIn {
|
||||
if ret.Expensive || ret.LongPolling {
|
||||
http.Redirect(w, req, setting.AppSubURL+"/user/login", http.StatusSeeOther)
|
||||
return
|
||||
}
|
||||
}
|
||||
next.ServeHTTP(w, req)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func isRoutePathExpensive(routePattern string) bool {
|
||||
if strings.HasPrefix(routePattern, "/user/") || strings.HasPrefix(routePattern, "/login/") {
|
||||
return false
|
||||
}
|
||||
|
||||
expensivePaths := []string{
|
||||
// code related
|
||||
"/{username}/{reponame}/archive/",
|
||||
"/{username}/{reponame}/blame/",
|
||||
"/{username}/{reponame}/commit/",
|
||||
"/{username}/{reponame}/commits/",
|
||||
"/{username}/{reponame}/graph",
|
||||
"/{username}/{reponame}/media/",
|
||||
"/{username}/{reponame}/raw/",
|
||||
"/{username}/{reponame}/src/",
|
||||
|
||||
// issue & PR related (no trailing slash)
|
||||
"/{username}/{reponame}/issues",
|
||||
"/{username}/{reponame}/{type:issues}",
|
||||
"/{username}/{reponame}/pulls",
|
||||
"/{username}/{reponame}/{type:pulls}",
|
||||
"/{username}/{reponame}/{type:issues|pulls}", // for 1.23 only
|
||||
|
||||
// wiki
|
||||
"/{username}/{reponame}/wiki/",
|
||||
|
||||
// activity
|
||||
"/{username}/{reponame}/activity/",
|
||||
}
|
||||
for _, path := range expensivePaths {
|
||||
if strings.HasPrefix(routePattern, path) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func isRoutePathForLongPolling(routePattern string) bool {
|
||||
return routePattern == "/user/events"
|
||||
}
|
||||
|
||||
func determineRequestPriority(ctx context.Context) (ret struct {
|
||||
SignedIn bool
|
||||
Expensive bool
|
||||
LongPolling bool
|
||||
},
|
||||
) {
|
||||
dataStore := middleware.GetContextData(ctx)
|
||||
chiRoutePath := chi.RouteContext(ctx).RoutePattern()
|
||||
if _, ok := dataStore[middleware.ContextDataKeySignedUser].(*user_model.User); ok {
|
||||
ret.SignedIn = true
|
||||
} else {
|
||||
ret.Expensive = isRoutePathExpensive(chiRoutePath)
|
||||
ret.LongPolling = isRoutePathForLongPolling(chiRoutePath)
|
||||
}
|
||||
return ret
|
||||
}
|
||||
30
routers/common/blockexpensive_test.go
Normal file
30
routers/common/blockexpensive_test.go
Normal file
@@ -0,0 +1,30 @@
|
||||
// Copyright 2025 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package common
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestBlockExpensive(t *testing.T) {
|
||||
cases := []struct {
|
||||
expensive bool
|
||||
routePath string
|
||||
}{
|
||||
{false, "/user/xxx"},
|
||||
{false, "/login/xxx"},
|
||||
{true, "/{username}/{reponame}/archive/xxx"},
|
||||
{true, "/{username}/{reponame}/graph"},
|
||||
{true, "/{username}/{reponame}/src/xxx"},
|
||||
{true, "/{username}/{reponame}/wiki/xxx"},
|
||||
{true, "/{username}/{reponame}/activity/xxx"},
|
||||
}
|
||||
for _, c := range cases {
|
||||
assert.Equal(t, c.expensive, isRoutePathExpensive(c.routePath), "routePath: %s", c.routePath)
|
||||
}
|
||||
|
||||
assert.True(t, isRoutePathForLongPolling("/user/events"))
|
||||
}
|
||||
@@ -156,7 +156,7 @@ func Install(ctx *context.Context) {
|
||||
form.DisableRegistration = setting.Service.DisableRegistration
|
||||
form.AllowOnlyExternalRegistration = setting.Service.AllowOnlyExternalRegistration
|
||||
form.EnableCaptcha = setting.Service.EnableCaptcha
|
||||
form.RequireSignInView = setting.Service.RequireSignInView
|
||||
form.RequireSignInView = setting.Service.RequireSignInViewStrict
|
||||
form.DefaultKeepEmailPrivate = setting.Service.DefaultKeepEmailPrivate
|
||||
form.DefaultAllowCreateOrganization = setting.Service.DefaultAllowCreateOrganization
|
||||
form.DefaultEnableTimetracking = setting.Service.DefaultEnableTimetracking
|
||||
|
||||
@@ -286,7 +286,7 @@ func ServCommand(ctx *context.PrivateContext) {
|
||||
repo.IsPrivate ||
|
||||
owner.Visibility.IsPrivate() ||
|
||||
(user != nil && user.IsRestricted) || // user will be nil if the key is a deploykey
|
||||
setting.Service.RequireSignInView) {
|
||||
setting.Service.RequireSignInViewStrict) {
|
||||
if key.Type == asymkey_model.KeyTypeDeploy {
|
||||
if deployKey.Mode < mode {
|
||||
ctx.JSON(http.StatusUnauthorized, private.Response{
|
||||
|
||||
@@ -4,26 +4,12 @@
|
||||
package web
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/modules/web"
|
||||
"code.gitea.io/gitea/routers/web/repo"
|
||||
"code.gitea.io/gitea/services/context"
|
||||
)
|
||||
|
||||
func addOwnerRepoGitHTTPRouters(m *web.Router) {
|
||||
reqGitSignIn := func(ctx *context.Context) {
|
||||
if !setting.Service.RequireSignInView {
|
||||
return
|
||||
}
|
||||
// rely on the results of Contexter
|
||||
if !ctx.IsSigned {
|
||||
// TODO: support digit auth - which would be Authorization header with digit
|
||||
ctx.Resp.Header().Set("WWW-Authenticate", `Basic realm="Gitea"`)
|
||||
ctx.Error(http.StatusUnauthorized)
|
||||
}
|
||||
}
|
||||
m.Group("/{username}/{reponame}", func() {
|
||||
m.Methods("POST,OPTIONS", "/git-upload-pack", repo.ServiceUploadPack)
|
||||
m.Methods("POST,OPTIONS", "/git-receive-pack", repo.ServiceReceivePack)
|
||||
@@ -36,5 +22,5 @@ func addOwnerRepoGitHTTPRouters(m *web.Router) {
|
||||
m.Methods("GET,OPTIONS", "/objects/{head:[0-9a-f]{2}}/{hash:[0-9a-f]{38,62}}", repo.GetLooseObject)
|
||||
m.Methods("GET,OPTIONS", "/objects/pack/pack-{file:[0-9a-f]{40,64}}.pack", repo.GetPackFile)
|
||||
m.Methods("GET,OPTIONS", "/objects/pack/pack-{file:[0-9a-f]{40,64}}.idx", repo.GetIdxFile)
|
||||
}, optSignInIgnoreCsrf, reqGitSignIn, repo.HTTPGitEnabledHandler, repo.CorsHandler(), context.UserAssignmentWeb())
|
||||
}, optSignInIgnoreCsrf, repo.HTTPGitEnabledHandler, repo.CorsHandler(), context.UserAssignmentWeb())
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ package actions
|
||||
import (
|
||||
"bytes"
|
||||
stdCtx "context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"slices"
|
||||
@@ -77,7 +78,11 @@ func List(ctx *context.Context) {
|
||||
return
|
||||
} else if !empty {
|
||||
commit, err := ctx.Repo.GitRepo.GetBranchCommit(ctx.Repo.Repository.DefaultBranch)
|
||||
if err != nil {
|
||||
if errors.Is(err, util.ErrNotExist) {
|
||||
ctx.Data["NotFoundPrompt"] = ctx.Tr("repo.branch.default_branch_not_exist", ctx.Repo.Repository.DefaultBranch)
|
||||
ctx.NotFound("GetBranchCommit", err)
|
||||
return
|
||||
} else if err != nil {
|
||||
ctx.ServerError("GetBranchCommit", err)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
"time"
|
||||
|
||||
activities_model "code.gitea.io/gitea/models/activities"
|
||||
"code.gitea.io/gitea/models/git"
|
||||
"code.gitea.io/gitea/models/unit"
|
||||
"code.gitea.io/gitea/modules/base"
|
||||
"code.gitea.io/gitea/services/context"
|
||||
@@ -52,12 +53,26 @@ func Activity(ctx *context.Context) {
|
||||
ctx.Data["DateUntil"] = timeUntil
|
||||
ctx.Data["PeriodText"] = ctx.Tr("repo.activity.period." + ctx.Data["Period"].(string))
|
||||
|
||||
canReadCode := ctx.Repo.CanRead(unit.TypeCode)
|
||||
if canReadCode {
|
||||
// GetActivityStats needs to read the default branch to get some information
|
||||
branchExist, _ := git.IsBranchExist(ctx, ctx.Repo.Repository.ID, ctx.Repo.Repository.DefaultBranch)
|
||||
if !branchExist {
|
||||
ctx.Data["NotFoundPrompt"] = ctx.Tr("repo.branch.default_branch_not_exist", ctx.Repo.Repository.DefaultBranch)
|
||||
ctx.NotFound("", nil)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
var err error
|
||||
if ctx.Data["Activity"], err = activities_model.GetActivityStats(ctx, ctx.Repo.Repository, timeFrom,
|
||||
// TODO: refactor these arguments to a struct
|
||||
ctx.Data["Activity"], err = activities_model.GetActivityStats(ctx, ctx.Repo.Repository, timeFrom,
|
||||
ctx.Repo.CanRead(unit.TypeReleases),
|
||||
ctx.Repo.CanRead(unit.TypeIssues),
|
||||
ctx.Repo.CanRead(unit.TypePullRequests),
|
||||
ctx.Repo.CanRead(unit.TypeCode)); err != nil {
|
||||
canReadCode,
|
||||
)
|
||||
if err != nil {
|
||||
ctx.ServerError("GetActivityStats", err)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -34,7 +34,7 @@ func CodeFrequencyData(ctx *context.Context) {
|
||||
ctx.Status(http.StatusAccepted)
|
||||
return
|
||||
}
|
||||
ctx.ServerError("GetCodeFrequencyData", err)
|
||||
ctx.ServerError("GetContributorStats", err)
|
||||
} else {
|
||||
ctx.JSON(http.StatusOK, contributorStats["total"].Weeks)
|
||||
}
|
||||
|
||||
@@ -127,7 +127,7 @@ func httpBase(ctx *context.Context) *serviceHandler {
|
||||
// Only public pull don't need auth.
|
||||
isPublicPull := repoExist && !repo.IsPrivate && isPull
|
||||
var (
|
||||
askAuth = !isPublicPull || setting.Service.RequireSignInView
|
||||
askAuth = !isPublicPull || setting.Service.RequireSignInViewStrict
|
||||
environ []string
|
||||
)
|
||||
|
||||
|
||||
@@ -4,12 +4,10 @@
|
||||
package repo
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
|
||||
"code.gitea.io/gitea/modules/base"
|
||||
"code.gitea.io/gitea/services/context"
|
||||
contributors_service "code.gitea.io/gitea/services/repository"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -26,16 +24,3 @@ func RecentCommits(ctx *context.Context) {
|
||||
|
||||
ctx.HTML(http.StatusOK, tplRecentCommits)
|
||||
}
|
||||
|
||||
// RecentCommitsData returns JSON of recent commits data
|
||||
func RecentCommitsData(ctx *context.Context) {
|
||||
if contributorStats, err := contributors_service.GetContributorStats(ctx, ctx.Cache, ctx.Repo.Repository, ctx.Repo.CommitID); err != nil {
|
||||
if errors.Is(err, contributors_service.ErrAwaitGeneration) {
|
||||
ctx.Status(http.StatusAccepted)
|
||||
return
|
||||
}
|
||||
ctx.ServerError("RecentCommitsData", err)
|
||||
} else {
|
||||
ctx.JSON(http.StatusOK, contributorStats["total"].Weeks)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,7 +9,6 @@ import (
|
||||
"image"
|
||||
"io"
|
||||
"path"
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
git_model "code.gitea.io/gitea/models/git"
|
||||
@@ -79,7 +78,7 @@ func prepareToRenderFile(ctx *context.Context, entry *git.TreeEntry) {
|
||||
if workFlowErr != nil {
|
||||
ctx.Data["FileError"] = ctx.Locale.Tr("actions.runs.invalid_workflow_helper", workFlowErr.Error())
|
||||
}
|
||||
} else if slices.Contains([]string{"CODEOWNERS", "docs/CODEOWNERS", ".gitea/CODEOWNERS"}, ctx.Repo.TreePath) {
|
||||
} else if issue_service.IsCodeOwnerFile(ctx.Repo.TreePath) {
|
||||
if data, err := blob.GetBlobContent(setting.UI.MaxDisplayFileSize); err == nil {
|
||||
_, warnings := issue_model.GetCodeOwnersFromContent(ctx, data)
|
||||
if len(warnings) > 0 {
|
||||
|
||||
@@ -285,23 +285,23 @@ func Routes() *web.Router {
|
||||
mid = append(mid, repo.GetActiveStopwatch)
|
||||
mid = append(mid, goGet)
|
||||
|
||||
others := web.NewRouter()
|
||||
others.Use(mid...)
|
||||
registerRoutes(others)
|
||||
routes.Mount("", others)
|
||||
webRoutes := web.NewRouter()
|
||||
webRoutes.Use(mid...)
|
||||
webRoutes.Group("", func() { registerWebRoutes(webRoutes) }, common.BlockExpensive())
|
||||
routes.Mount("", webRoutes)
|
||||
return routes
|
||||
}
|
||||
|
||||
var optSignInIgnoreCsrf = verifyAuthWithOptions(&common.VerifyOptions{DisableCSRF: true})
|
||||
|
||||
// registerRoutes register routes
|
||||
func registerRoutes(m *web.Router) {
|
||||
// registerWebRoutes register routes
|
||||
func registerWebRoutes(m *web.Router) {
|
||||
// required to be signed in or signed out
|
||||
reqSignIn := verifyAuthWithOptions(&common.VerifyOptions{SignInRequired: true})
|
||||
reqSignOut := verifyAuthWithOptions(&common.VerifyOptions{SignOutRequired: true})
|
||||
// optional sign in (if signed in, use the user as doer, if not, no doer)
|
||||
optSignIn := verifyAuthWithOptions(&common.VerifyOptions{SignInRequired: setting.Service.RequireSignInView})
|
||||
optExploreSignIn := verifyAuthWithOptions(&common.VerifyOptions{SignInRequired: setting.Service.RequireSignInView || setting.Service.Explore.RequireSigninView})
|
||||
optSignIn := verifyAuthWithOptions(&common.VerifyOptions{SignInRequired: setting.Service.RequireSignInViewStrict})
|
||||
optExploreSignIn := verifyAuthWithOptions(&common.VerifyOptions{SignInRequired: setting.Service.RequireSignInViewStrict || setting.Service.Explore.RequireSigninView})
|
||||
|
||||
validation.AddBindingRules()
|
||||
|
||||
@@ -1454,20 +1454,23 @@ func registerRoutes(m *web.Router) {
|
||||
m.Group("/{username}/{reponame}/activity", func() {
|
||||
m.Get("", repo.Activity)
|
||||
m.Get("/{period}", repo.Activity)
|
||||
m.Group("/contributors", func() {
|
||||
m.Get("", repo.Contributors)
|
||||
m.Get("/data", repo.ContributorsData)
|
||||
})
|
||||
m.Group("/code-frequency", func() {
|
||||
m.Get("", repo.CodeFrequency)
|
||||
m.Get("/data", repo.CodeFrequencyData)
|
||||
})
|
||||
m.Group("/recent-commits", func() {
|
||||
m.Get("", repo.RecentCommits)
|
||||
m.Get("/data", repo.RecentCommitsData)
|
||||
})
|
||||
|
||||
m.Group("", func() {
|
||||
m.Group("/contributors", func() {
|
||||
m.Get("", repo.Contributors)
|
||||
m.Get("/data", repo.ContributorsData)
|
||||
})
|
||||
m.Group("/code-frequency", func() {
|
||||
m.Get("", repo.CodeFrequency)
|
||||
m.Get("/data", repo.CodeFrequencyData)
|
||||
})
|
||||
m.Group("/recent-commits", func() {
|
||||
m.Get("", repo.RecentCommits)
|
||||
m.Get("/data", repo.CodeFrequencyData) // "recent-commits" also uses the same data as "code-frequency"
|
||||
})
|
||||
}, reqRepoCodeReader)
|
||||
},
|
||||
optSignIn, context.RepoAssignment, context.RequireRepoReaderOr(unit.TypePullRequests, unit.TypeIssues, unit.TypeReleases),
|
||||
optSignIn, context.RepoAssignment, context.RequireRepoReaderOr(unit.TypeCode, unit.TypePullRequests, unit.TypeIssues, unit.TypeReleases),
|
||||
context.RepoRef(), repo.MustBeNotEmpty,
|
||||
)
|
||||
// end "/{username}/{reponame}/activity"
|
||||
|
||||
@@ -151,7 +151,7 @@ func (ctx *Context) notFoundInternal(logMsg string, logErr error) {
|
||||
|
||||
ctx.Data["IsRepo"] = ctx.Repo.Repository != nil
|
||||
ctx.Data["Title"] = "Page Not Found"
|
||||
ctx.HTML(http.StatusNotFound, base.TplName("status/404"))
|
||||
ctx.HTML(http.StatusNotFound, "status/404")
|
||||
}
|
||||
|
||||
// ServerError displays a 500 (Internal Server Error) page and prints the given error, if any.
|
||||
|
||||
@@ -93,7 +93,7 @@ func packageAssignment(ctx *packageAssignmentCtx, errCb func(int, string, any))
|
||||
}
|
||||
|
||||
func determineAccessMode(ctx *Base, pkg *Package, doer *user_model.User) (perm.AccessMode, error) {
|
||||
if setting.Service.RequireSignInView && (doer == nil || doer.IsGhost()) {
|
||||
if setting.Service.RequireSignInViewStrict && (doer == nil || doer.IsGhost()) {
|
||||
return perm.AccessModeNone, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -356,7 +356,9 @@ func RedirectToRepo(ctx *Base, redirectRepoID int64) {
|
||||
if ctx.Req.URL.RawQuery != "" {
|
||||
redirectPath += "?" + ctx.Req.URL.RawQuery
|
||||
}
|
||||
ctx.Redirect(path.Join(setting.AppSubURL, redirectPath), http.StatusTemporaryRedirect)
|
||||
// Git client needs a 301 redirect by default to follow the new location
|
||||
// It's not documentated in git documentation, but it's the behavior of git client
|
||||
ctx.Redirect(path.Join(setting.AppSubURL, redirectPath), http.StatusMovedPermanently)
|
||||
}
|
||||
|
||||
func repoAssignment(ctx *Context, repo *repo_model.Repository) {
|
||||
|
||||
@@ -121,7 +121,7 @@ func checkStorage(opts *checkStorageOptions) func(ctx context.Context, logger lo
|
||||
storer: storage.LFS,
|
||||
isOrphaned: func(path string, obj storage.Object, stat fs.FileInfo) (bool, error) {
|
||||
// The oid of an LFS stored object is the name but with all the path.Separators removed
|
||||
oid := strings.ReplaceAll(path, "/", "")
|
||||
oid := strings.ReplaceAll(strings.ReplaceAll(path, "\\", ""), "/", "")
|
||||
exists, err := git.ExistsLFSObject(ctx, oid)
|
||||
return !exists, err
|
||||
},
|
||||
|
||||
@@ -92,8 +92,12 @@ func ChangeTitle(ctx context.Context, issue *issues_model.Issue, doer *user_mode
|
||||
|
||||
var reviewNotifiers []*ReviewRequestNotifier
|
||||
if issue.IsPull && issues_model.HasWorkInProgressPrefix(oldTitle) && !issues_model.HasWorkInProgressPrefix(title) {
|
||||
if err := issue.LoadPullRequest(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var err error
|
||||
reviewNotifiers, err = PullRequestCodeOwnersReview(ctx, issue, issue.PullRequest)
|
||||
reviewNotifiers, err = PullRequestCodeOwnersReview(ctx, issue.PullRequest)
|
||||
if err != nil {
|
||||
log.Error("PullRequestCodeOwnersReview: %v", err)
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ package issue
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"slices"
|
||||
"time"
|
||||
|
||||
issues_model "code.gitea.io/gitea/models/issues"
|
||||
@@ -40,20 +41,31 @@ type ReviewRequestNotifier struct {
|
||||
ReviewTeam *org_model.Team
|
||||
}
|
||||
|
||||
func PullRequestCodeOwnersReview(ctx context.Context, issue *issues_model.Issue, pr *issues_model.PullRequest) ([]*ReviewRequestNotifier, error) {
|
||||
files := []string{"CODEOWNERS", "docs/CODEOWNERS", ".gitea/CODEOWNERS"}
|
||||
var codeOwnerFiles = []string{"CODEOWNERS", "docs/CODEOWNERS", ".gitea/CODEOWNERS"}
|
||||
|
||||
func IsCodeOwnerFile(f string) bool {
|
||||
return slices.Contains(codeOwnerFiles, f)
|
||||
}
|
||||
|
||||
func PullRequestCodeOwnersReview(ctx context.Context, pr *issues_model.PullRequest) ([]*ReviewRequestNotifier, error) {
|
||||
return PullRequestCodeOwnersReviewSpecialCommits(ctx, pr, "", "") // no commit is provided, then it uses PR's base&head branch
|
||||
}
|
||||
|
||||
func PullRequestCodeOwnersReviewSpecialCommits(ctx context.Context, pr *issues_model.PullRequest, startCommitID, endCommitID string) ([]*ReviewRequestNotifier, error) {
|
||||
if err := pr.LoadIssue(ctx); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
issue := pr.Issue
|
||||
if pr.IsWorkInProgress(ctx) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
if err := pr.LoadHeadRepo(ctx); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := pr.LoadBaseRepo(ctx); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
pr.Issue.Repo = pr.BaseRepo
|
||||
|
||||
if pr.BaseRepo.IsFork {
|
||||
return nil, nil
|
||||
@@ -71,7 +83,7 @@ func PullRequestCodeOwnersReview(ctx context.Context, issue *issues_model.Issue,
|
||||
}
|
||||
|
||||
var data string
|
||||
for _, file := range files {
|
||||
for _, file := range codeOwnerFiles {
|
||||
if blob, err := commit.GetBlobByPath(file); err == nil {
|
||||
data, err = blob.GetBlobContent(setting.UI.MaxDisplayFileSize)
|
||||
if err == nil {
|
||||
@@ -79,18 +91,28 @@ func PullRequestCodeOwnersReview(ctx context.Context, issue *issues_model.Issue,
|
||||
}
|
||||
}
|
||||
}
|
||||
if data == "" {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
rules, _ := issues_model.GetCodeOwnersFromContent(ctx, data)
|
||||
if len(rules) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// get the mergebase
|
||||
mergeBase, err := getMergeBase(repo, pr, git.BranchPrefix+pr.BaseBranch, pr.GetGitRefName())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
if startCommitID == "" && endCommitID == "" {
|
||||
// get the mergebase
|
||||
mergeBase, err := getMergeBase(repo, pr, git.BranchPrefix+pr.BaseBranch, pr.GetGitRefName())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
startCommitID = mergeBase
|
||||
endCommitID = pr.GetGitRefName()
|
||||
}
|
||||
|
||||
// https://github.com/go-gitea/gitea/issues/29763, we need to get the files changed
|
||||
// between the merge base and the head commit but not the base branch and the head commit
|
||||
changedFiles, err := repo.GetFilesChangedBetween(mergeBase, pr.GetGitRefName())
|
||||
changedFiles, err := repo.GetFilesChangedBetween(startCommitID, endCommitID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -248,7 +248,7 @@ func createOrUpdateConfigFile(ctx context.Context, repo *repo_model.Repository,
|
||||
"Initialize Cargo Config",
|
||||
func(t *files_service.TemporaryUploadRepository) error {
|
||||
var b bytes.Buffer
|
||||
err := json.NewEncoder(&b).Encode(BuildConfig(owner, setting.Service.RequireSignInView || owner.Visibility != structs.VisibleTypePublic || repo.IsPrivate))
|
||||
err := json.NewEncoder(&b).Encode(BuildConfig(owner, setting.Service.RequireSignInViewStrict || owner.Visibility != structs.VisibleTypePublic || repo.IsPrivate))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -408,7 +408,6 @@ func buildPrimary(ctx context.Context, pv *packages_model.PackageVersion, pfs []
|
||||
files = append(files, f)
|
||||
}
|
||||
}
|
||||
packageVersion := fmt.Sprintf("%s-%s", pd.FileMetadata.Version, pd.FileMetadata.Release)
|
||||
packages = append(packages, &Package{
|
||||
Type: "rpm",
|
||||
Name: pd.Package.Name,
|
||||
@@ -437,7 +436,7 @@ func buildPrimary(ctx context.Context, pv *packages_model.PackageVersion, pfs []
|
||||
Archive: pd.FileMetadata.ArchiveSize,
|
||||
},
|
||||
Location: Location{
|
||||
Href: fmt.Sprintf("package/%s/%s/%s/%s-%s.%s.rpm", pd.Package.Name, packageVersion, pd.FileMetadata.Architecture, pd.Package.Name, packageVersion, pd.FileMetadata.Architecture),
|
||||
Href: fmt.Sprintf("package/%s/%s/%s/%s-%s.%s.rpm", pd.Package.Name, pd.Version.Version, pd.FileMetadata.Architecture, pd.Package.Name, pd.Version.Version, pd.FileMetadata.Architecture),
|
||||
},
|
||||
Format: Format{
|
||||
License: pd.VersionMetadata.License,
|
||||
|
||||
@@ -189,7 +189,15 @@ func Merge(ctx context.Context, pr *issues_model.PullRequest, doer *user_model.U
|
||||
}
|
||||
defer releaser()
|
||||
defer func() {
|
||||
go AddTestPullRequestTask(doer, pr.BaseRepo.ID, pr.BaseBranch, false, "", "")
|
||||
go AddTestPullRequestTask(TestPullRequestOptions{
|
||||
RepoID: pr.BaseRepo.ID,
|
||||
Doer: doer,
|
||||
Branch: pr.BaseBranch,
|
||||
IsSync: false,
|
||||
IsForcePush: false,
|
||||
OldCommitID: "",
|
||||
NewCommitID: "",
|
||||
})
|
||||
}()
|
||||
|
||||
_, err = doMergeAndPush(ctx, pr, doer, mergeStyle, expectedHeadCommitID, message, repo_module.PushTriggerPRMergeToBase)
|
||||
|
||||
@@ -176,7 +176,7 @@ func NewPullRequest(ctx context.Context, opts *NewPullRequestOptions) error {
|
||||
}
|
||||
|
||||
if !pr.IsWorkInProgress(ctx) {
|
||||
reviewNotifiers, err = issue_service.PullRequestCodeOwnersReview(ctx, issue, pr)
|
||||
reviewNotifiers, err = issue_service.PullRequestCodeOwnersReview(ctx, pr)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -349,19 +349,29 @@ func checkForInvalidation(ctx context.Context, requests issues_model.PullRequest
|
||||
return nil
|
||||
}
|
||||
|
||||
type TestPullRequestOptions struct {
|
||||
RepoID int64
|
||||
Doer *user_model.User
|
||||
Branch string
|
||||
IsSync bool // True means it's a pull request synchronization, false means it's triggered for pull request merging or updating
|
||||
IsForcePush bool
|
||||
OldCommitID string
|
||||
NewCommitID string
|
||||
}
|
||||
|
||||
// AddTestPullRequestTask adds new test tasks by given head/base repository and head/base branch,
|
||||
// and generate new patch for testing as needed.
|
||||
func AddTestPullRequestTask(doer *user_model.User, repoID int64, branch string, isSync bool, oldCommitID, newCommitID string) {
|
||||
log.Trace("AddTestPullRequestTask [head_repo_id: %d, head_branch: %s]: finding pull requests", repoID, branch)
|
||||
func AddTestPullRequestTask(opts TestPullRequestOptions) {
|
||||
log.Trace("AddTestPullRequestTask [head_repo_id: %d, head_branch: %s]: finding pull requests", opts.RepoID, opts.Branch)
|
||||
graceful.GetManager().RunWithShutdownContext(func(ctx context.Context) {
|
||||
// There is no sensible way to shut this down ":-("
|
||||
// If you don't let it run all the way then you will lose data
|
||||
// TODO: graceful: AddTestPullRequestTask needs to become a queue!
|
||||
|
||||
// GetUnmergedPullRequestsByHeadInfo() only return open and unmerged PR.
|
||||
prs, err := issues_model.GetUnmergedPullRequestsByHeadInfo(ctx, repoID, branch)
|
||||
prs, err := issues_model.GetUnmergedPullRequestsByHeadInfo(ctx, opts.RepoID, opts.Branch)
|
||||
if err != nil {
|
||||
log.Error("Find pull requests [head_repo_id: %d, head_branch: %s]: %v", repoID, branch, err)
|
||||
log.Error("Find pull requests [head_repo_id: %d, head_branch: %s]: %v", opts.RepoID, opts.Branch, err)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -377,25 +387,24 @@ func AddTestPullRequestTask(doer *user_model.User, repoID int64, branch string,
|
||||
}
|
||||
|
||||
AddToTaskQueue(ctx, pr)
|
||||
comment, err := CreatePushPullComment(ctx, doer, pr, oldCommitID, newCommitID)
|
||||
comment, err := CreatePushPullComment(ctx, opts.Doer, pr, opts.OldCommitID, opts.NewCommitID)
|
||||
if err == nil && comment != nil {
|
||||
notify_service.PullRequestPushCommits(ctx, doer, pr, comment)
|
||||
notify_service.PullRequestPushCommits(ctx, opts.Doer, pr, comment)
|
||||
}
|
||||
}
|
||||
|
||||
if isSync {
|
||||
requests := issues_model.PullRequestList(prs)
|
||||
if err = requests.LoadAttributes(ctx); err != nil {
|
||||
if opts.IsSync {
|
||||
if err = issues_model.PullRequestList(prs).LoadAttributes(ctx); err != nil {
|
||||
log.Error("PullRequestList.LoadAttributes: %v", err)
|
||||
}
|
||||
if invalidationErr := checkForInvalidation(ctx, requests, repoID, doer, branch); invalidationErr != nil {
|
||||
if invalidationErr := checkForInvalidation(ctx, prs, opts.RepoID, opts.Doer, opts.Branch); invalidationErr != nil {
|
||||
log.Error("checkForInvalidation: %v", invalidationErr)
|
||||
}
|
||||
if err == nil {
|
||||
for _, pr := range prs {
|
||||
objectFormat := git.ObjectFormatFromName(pr.BaseRepo.ObjectFormatName)
|
||||
if newCommitID != "" && newCommitID != objectFormat.EmptyObjectID().String() {
|
||||
changed, err := checkIfPRContentChanged(ctx, pr, oldCommitID, newCommitID)
|
||||
if opts.NewCommitID != "" && opts.NewCommitID != objectFormat.EmptyObjectID().String() {
|
||||
changed, err := checkIfPRContentChanged(ctx, pr, opts.OldCommitID, opts.NewCommitID)
|
||||
if err != nil {
|
||||
log.Error("checkIfPRContentChanged: %v", err)
|
||||
}
|
||||
@@ -411,12 +420,12 @@ func AddTestPullRequestTask(doer *user_model.User, repoID int64, branch string,
|
||||
log.Error("GetFirstMatchProtectedBranchRule: %v", err)
|
||||
}
|
||||
if pb != nil && pb.DismissStaleApprovals {
|
||||
if err := DismissApprovalReviews(ctx, doer, pr); err != nil {
|
||||
if err := DismissApprovalReviews(ctx, opts.Doer, pr); err != nil {
|
||||
log.Error("DismissApprovalReviews: %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
if err := issues_model.MarkReviewsAsNotStale(ctx, pr.IssueID, newCommitID); err != nil {
|
||||
if err := issues_model.MarkReviewsAsNotStale(ctx, pr.IssueID, opts.NewCommitID); err != nil {
|
||||
log.Error("MarkReviewsAsNotStale: %v", err)
|
||||
}
|
||||
divergence, err := GetDiverging(ctx, pr)
|
||||
@@ -430,15 +439,30 @@ func AddTestPullRequestTask(doer *user_model.User, repoID int64, branch string,
|
||||
}
|
||||
}
|
||||
|
||||
notify_service.PullRequestSynchronized(ctx, doer, pr)
|
||||
if !pr.IsWorkInProgress(ctx) {
|
||||
var reviewNotifiers []*issue_service.ReviewRequestNotifier
|
||||
if opts.IsForcePush {
|
||||
reviewNotifiers, err = issue_service.PullRequestCodeOwnersReview(ctx, pr)
|
||||
} else {
|
||||
reviewNotifiers, err = issue_service.PullRequestCodeOwnersReviewSpecialCommits(ctx, pr, opts.OldCommitID, opts.NewCommitID)
|
||||
}
|
||||
if err != nil {
|
||||
log.Error("PullRequestCodeOwnersReview: %v", err)
|
||||
}
|
||||
if len(reviewNotifiers) > 0 {
|
||||
issue_service.ReviewRequestNotify(ctx, pr.Issue, opts.Doer, reviewNotifiers)
|
||||
}
|
||||
}
|
||||
|
||||
notify_service.PullRequestSynchronized(ctx, opts.Doer, pr)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
log.Trace("AddTestPullRequestTask [base_repo_id: %d, base_branch: %s]: finding pull requests", repoID, branch)
|
||||
prs, err = issues_model.GetUnmergedPullRequestsByBaseInfo(ctx, repoID, branch)
|
||||
log.Trace("AddTestPullRequestTask [base_repo_id: %d, base_branch: %s]: finding pull requests", opts.RepoID, opts.Branch)
|
||||
prs, err = issues_model.GetUnmergedPullRequestsByBaseInfo(ctx, opts.RepoID, opts.Branch)
|
||||
if err != nil {
|
||||
log.Error("Find pull requests [base_repo_id: %d, base_branch: %s]: %v", repoID, branch, err)
|
||||
log.Error("Find pull requests [base_repo_id: %d, base_branch: %s]: %v", opts.RepoID, opts.Branch, err)
|
||||
return
|
||||
}
|
||||
for _, pr := range prs {
|
||||
|
||||
@@ -42,7 +42,15 @@ func Update(ctx context.Context, pr *issues_model.PullRequest, doer *user_model.
|
||||
|
||||
if rebase {
|
||||
defer func() {
|
||||
go AddTestPullRequestTask(doer, pr.BaseRepo.ID, pr.BaseBranch, false, "", "")
|
||||
go AddTestPullRequestTask(TestPullRequestOptions{
|
||||
RepoID: pr.BaseRepo.ID,
|
||||
Doer: doer,
|
||||
Branch: pr.BaseBranch,
|
||||
IsSync: false,
|
||||
IsForcePush: false,
|
||||
OldCommitID: "",
|
||||
NewCommitID: "",
|
||||
})
|
||||
}()
|
||||
|
||||
return updateHeadByRebaseOnToBase(ctx, pr, doer)
|
||||
@@ -83,7 +91,15 @@ func Update(ctx context.Context, pr *issues_model.PullRequest, doer *user_model.
|
||||
_, err = doMergeAndPush(ctx, reversePR, doer, repo_model.MergeStyleMerge, "", message, repository.PushTriggerPRUpdateWithBase)
|
||||
|
||||
defer func() {
|
||||
go AddTestPullRequestTask(doer, reversePR.HeadRepo.ID, reversePR.HeadBranch, false, "", "")
|
||||
go AddTestPullRequestTask(TestPullRequestOptions{
|
||||
RepoID: reversePR.HeadRepo.ID,
|
||||
Doer: doer,
|
||||
Branch: reversePR.HeadBranch,
|
||||
IsSync: false,
|
||||
IsForcePush: false,
|
||||
OldCommitID: "",
|
||||
NewCommitID: "",
|
||||
})
|
||||
}()
|
||||
|
||||
return err
|
||||
|
||||
@@ -170,7 +170,6 @@ func pushUpdates(optsList []*repo_module.PushUpdateOptions) error {
|
||||
branch := opts.RefFullName.BranchName()
|
||||
if !opts.IsDelRef() {
|
||||
log.Trace("TriggerTask '%s/%s' by %s", repo.Name, branch, pusher.Name)
|
||||
go pull_service.AddTestPullRequestTask(pusher, repo.ID, branch, true, opts.OldCommitID, opts.NewCommitID)
|
||||
|
||||
newCommit, err := gitRepo.GetCommit(opts.NewCommitID)
|
||||
if err != nil {
|
||||
@@ -212,6 +211,17 @@ func pushUpdates(optsList []*repo_module.PushUpdateOptions) error {
|
||||
log.Error("IsForcePush %s:%s failed: %v", repo.FullName(), branch, err)
|
||||
}
|
||||
|
||||
// only update branch can trigger pull request task because the pull request hasn't been created yet when creaing a branch
|
||||
go pull_service.AddTestPullRequestTask(pull_service.TestPullRequestOptions{
|
||||
RepoID: repo.ID,
|
||||
Doer: pusher,
|
||||
Branch: branch,
|
||||
IsSync: true,
|
||||
IsForcePush: isForcePush,
|
||||
OldCommitID: opts.OldCommitID,
|
||||
NewCommitID: opts.NewCommitID,
|
||||
})
|
||||
|
||||
if isForcePush {
|
||||
log.Trace("Push %s is a force push", opts.NewCommitID)
|
||||
|
||||
|
||||
@@ -14,6 +14,7 @@ import (
|
||||
"unicode/utf8"
|
||||
|
||||
webhook_model "code.gitea.io/gitea/models/webhook"
|
||||
"code.gitea.io/gitea/modules/base"
|
||||
"code.gitea.io/gitea/modules/git"
|
||||
"code.gitea.io/gitea/modules/json"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
@@ -101,6 +102,13 @@ var (
|
||||
redColor = color("ff3232")
|
||||
)
|
||||
|
||||
// https://discord.com/developers/docs/resources/message#embed-object-embed-limits
|
||||
// Discord has some limits in place for the embeds.
|
||||
// According to some tests, there is no consistent limit for different character sets.
|
||||
// For example: 4096 ASCII letters are allowed, but only 2490 emoji characters are allowed.
|
||||
// To keep it simple, we currently truncate at 2000.
|
||||
const discordDescriptionCharactersLimit = 2000
|
||||
|
||||
type discordConvertor struct {
|
||||
Username string
|
||||
AvatarURL string
|
||||
@@ -307,7 +315,7 @@ func (d discordConvertor) createPayload(s *api.User, title, text, url string, co
|
||||
Embeds: []DiscordEmbed{
|
||||
{
|
||||
Title: title,
|
||||
Description: text,
|
||||
Description: base.TruncateString(text, discordDescriptionCharactersLimit),
|
||||
URL: url,
|
||||
Color: color,
|
||||
Author: DiscordEmbedAuthor{
|
||||
|
||||
@@ -148,7 +148,7 @@
|
||||
<dt>{{ctx.Locale.Tr "admin.config.enable_openid_signin"}}</dt>
|
||||
<dd>{{svg (Iif .Service.EnableOpenIDSignIn "octicon-check" "octicon-x")}}</dd>
|
||||
<dt>{{ctx.Locale.Tr "admin.config.require_sign_in_view"}}</dt>
|
||||
<dd>{{svg (Iif .Service.RequireSignInView "octicon-check" "octicon-x")}}</dd>
|
||||
<dd>{{svg (Iif .Service.RequireSignInViewStrict "octicon-check" "octicon-x")}}</dd>
|
||||
<dt>{{ctx.Locale.Tr "admin.config.mail_notify"}}</dt>
|
||||
<dd>{{svg (Iif .Service.EnableNotifyMail "octicon-check" "octicon-x")}}</dd>
|
||||
<dt>{{ctx.Locale.Tr "admin.config.enable_captcha"}}</dt>
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
{{$canWriteProject := and .CanWriteProjects (or (not .Repository) (not .Repository.IsArchived))}}
|
||||
|
||||
<div class="ui container tw-max-w-full">
|
||||
<div class="tw-flex tw-justify-between tw-items-center tw-mb-4 tw-gap-3">
|
||||
<h2 class="tw-mb-0 tw-flex-1 tw-break-anywhere">{{.Project.Title}}</h2>
|
||||
<div class="project-toolbar-right">
|
||||
<div class="ui secondary filter menu labels">
|
||||
<div class="flex-text-block tw-flex-wrap tw-mb-4">
|
||||
<h2 class="tw-mb-0">{{.Project.Title}}</h2>
|
||||
<div class="tw-flex-1"></div>
|
||||
<div class="ui secondary menu tw-m-0">
|
||||
{{$queryLink := QueryBuild "?" "labels" .SelectLabels "assignee" $.AssigneeID "archived_labels" (Iif $.ShowArchivedLabels "true")}}
|
||||
|
||||
{{template "repo/issue/filter_item_label" dict "Labels" .Labels "QueryLink" $queryLink "SupportArchivedLabel" true}}
|
||||
@@ -19,7 +19,6 @@
|
||||
"TextNegativeOne" (ctx.Locale.Tr "repo.issues.filter_assginee_no_assignee")
|
||||
}}
|
||||
</div>
|
||||
</div>
|
||||
{{if $canWriteProject}}
|
||||
<div class="ui compact mini menu">
|
||||
<a class="item" href="{{.Link}}/edit?redirect=project">
|
||||
|
||||
@@ -10,12 +10,7 @@
|
||||
<div class="ui attached segment">
|
||||
{{template "base/alert" .}}
|
||||
{{template "repo/create_helper" .}}
|
||||
|
||||
{{if not .CanCreateRepo}}
|
||||
<div class="ui negative message">
|
||||
<p>{{ctx.Locale.TrN .MaxCreationLimit "repo.form.reach_limit_of_creation_1" "repo.form.reach_limit_of_creation_n" .MaxCreationLimit}}</p>
|
||||
</div>
|
||||
{{end}}
|
||||
<div id="create-repo-error-message" class="ui negative message tw-text-center tw-hidden"></div>
|
||||
<div class="inline required field {{if .Err_Owner}}error{{end}}">
|
||||
<label>{{ctx.Locale.Tr "repo.owner"}}</label>
|
||||
<div class="ui selection owner dropdown">
|
||||
@@ -26,7 +21,11 @@
|
||||
</span>
|
||||
{{svg "octicon-triangle-down" 14 "dropdown icon"}}
|
||||
<div class="menu">
|
||||
<div class="item truncated-item-container" data-value="{{.SignedUser.ID}}" title="{{.SignedUser.Name}}">
|
||||
<div class="item truncated-item-container" data-value="{{.SignedUser.ID}}" title="{{.SignedUser.Name}}"
|
||||
{{if not .CanCreateRepo}}
|
||||
data-create-repo-disallowed-prompt="{{ctx.Locale.TrN .MaxCreationLimit "repo.form.reach_limit_of_creation_1" "repo.form.reach_limit_of_creation_n" .MaxCreationLimit}}"
|
||||
{{end}}
|
||||
>
|
||||
{{ctx.AvatarUtils.Avatar .SignedUser 28 "mini"}}
|
||||
<span class="truncated-item-name">{{.SignedUser.ShortName 40}}</span>
|
||||
</div>
|
||||
@@ -209,7 +208,7 @@
|
||||
<br>
|
||||
<div class="inline field">
|
||||
<label></label>
|
||||
<button class="ui primary button{{if not .CanCreateRepo}} disabled{{end}}">
|
||||
<button class="ui primary button">
|
||||
{{ctx.Locale.Tr "repo.create_repo"}}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -14,9 +14,10 @@
|
||||
</div>
|
||||
{{end}}
|
||||
|
||||
<div class="list-header">
|
||||
{{template "repo/issue/navbar" .}}
|
||||
<div class="list-header flex-text-block">
|
||||
{{template "repo/issue/search" .}}
|
||||
<a class="ui small button" href="{{.RepoLink}}/labels">{{ctx.Locale.Tr "repo.labels"}}</a>
|
||||
<a class="ui small button" href="{{.RepoLink}}/milestones">{{ctx.Locale.Tr "repo.milestones"}}</a>
|
||||
{{if not .Repository.IsArchived}}
|
||||
{{if .PageIsIssueList}}
|
||||
<a class="ui small primary button issue-list-new" href="{{.RepoLink}}/issues/new{{if .NewIssueChooseTemplate}}/choose{{end}}">{{ctx.Locale.Tr "repo.issues.new"}}</a>
|
||||
|
||||
@@ -1,14 +1,18 @@
|
||||
{{$canReadCode := $.Permission.CanRead ctx.Consts.RepoUnitTypeCode}}
|
||||
|
||||
<div class="ui fluid vertical menu">
|
||||
<a class="{{if .PageIsPulse}}active {{end}}item" href="{{.RepoLink}}/activity">
|
||||
{{ctx.Locale.Tr "repo.activity.navbar.pulse"}}
|
||||
</a>
|
||||
<a class="{{if .PageIsContributors}}active {{end}}item" href="{{.RepoLink}}/activity/contributors">
|
||||
{{ctx.Locale.Tr "repo.activity.navbar.contributors"}}
|
||||
</a>
|
||||
<a class="{{if .PageIsCodeFrequency}}active{{end}} item" href="{{.RepoLink}}/activity/code-frequency">
|
||||
{{ctx.Locale.Tr "repo.activity.navbar.code_frequency"}}
|
||||
</a>
|
||||
<a class="{{if .PageIsRecentCommits}}active{{end}} item" href="{{.RepoLink}}/activity/recent-commits">
|
||||
{{ctx.Locale.Tr "repo.activity.navbar.recent_commits"}}
|
||||
</a>
|
||||
{{if $canReadCode}}
|
||||
<a class="{{if .PageIsContributors}}active {{end}}item" href="{{.RepoLink}}/activity/contributors">
|
||||
{{ctx.Locale.Tr "repo.activity.navbar.contributors"}}
|
||||
</a>
|
||||
<a class="{{if .PageIsCodeFrequency}}active{{end}} item" href="{{.RepoLink}}/activity/code-frequency">
|
||||
{{ctx.Locale.Tr "repo.activity.navbar.code_frequency"}}
|
||||
</a>
|
||||
<a class="{{if .PageIsRecentCommits}}active{{end}} item" href="{{.RepoLink}}/activity/recent-commits">
|
||||
{{ctx.Locale.Tr "repo.activity.navbar.recent_commits"}}
|
||||
</a>
|
||||
{{end}}
|
||||
</div>
|
||||
|
||||
@@ -2,8 +2,9 @@
|
||||
<div role="main" aria-label="{{.Title}}" class="page-content repository projects view-project">
|
||||
{{template "repo/header" .}}
|
||||
<div class="ui container padded">
|
||||
<div class="tw-flex tw-justify-between tw-items-center tw-mb-4">
|
||||
{{template "repo/issue/navbar" .}}
|
||||
<div class="flex-text-block tw-justify-end tw-mb-4">
|
||||
<a class="ui small button" href="{{.RepoLink}}/labels">{{ctx.Locale.Tr "repo.labels"}}</a>
|
||||
<a class="ui small button" href="{{.RepoLink}}/milestones">{{ctx.Locale.Tr "repo.milestones"}}</a>
|
||||
<a class="ui small primary button" href="{{.RepoLink}}/issues/new/choose?project={{.Project.ID}}">{{ctx.Locale.Tr "repo.issues.new"}}</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -4,7 +4,9 @@
|
||||
package integration
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
"testing"
|
||||
@@ -24,7 +26,9 @@ import (
|
||||
"code.gitea.io/gitea/modules/git"
|
||||
"code.gitea.io/gitea/modules/gitrepo"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
api "code.gitea.io/gitea/modules/structs"
|
||||
"code.gitea.io/gitea/modules/test"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
pull_service "code.gitea.io/gitea/services/pull"
|
||||
release_service "code.gitea.io/gitea/services/release"
|
||||
repo_service "code.gitea.io/gitea/services/repository"
|
||||
@@ -451,3 +455,109 @@ func TestCreateDeleteRefEvent(t *testing.T) {
|
||||
assert.NotNil(t, run)
|
||||
})
|
||||
}
|
||||
|
||||
func TestClosePullRequestWithPath(t *testing.T) {
|
||||
onGiteaRun(t, func(t *testing.T, u *url.URL) {
|
||||
// user2 is the owner of the base repo
|
||||
user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
|
||||
user2Token := getTokenForLoggedInUser(t, loginUser(t, user2.Name), auth_model.AccessTokenScopeWriteRepository, auth_model.AccessTokenScopeWriteUser)
|
||||
// user4 is the owner of the fork repo
|
||||
user4 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 4})
|
||||
user4Token := getTokenForLoggedInUser(t, loginUser(t, user4.Name), auth_model.AccessTokenScopeWriteRepository, auth_model.AccessTokenScopeWriteUser)
|
||||
|
||||
// create the base repo
|
||||
req := NewRequestWithJSON(t, "POST", "/api/v1/user/repos", &api.CreateRepoOption{
|
||||
Name: "close-pull-request-with-path",
|
||||
Private: false,
|
||||
Readme: "Default",
|
||||
AutoInit: true,
|
||||
DefaultBranch: "main",
|
||||
}).AddTokenAuth(user2Token)
|
||||
resp := MakeRequest(t, req, http.StatusCreated)
|
||||
var apiBaseRepo api.Repository
|
||||
DecodeJSON(t, resp, &apiBaseRepo)
|
||||
baseRepo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: apiBaseRepo.ID})
|
||||
user2APICtx := NewAPITestContext(t, baseRepo.OwnerName, baseRepo.Name, auth_model.AccessTokenScopeWriteRepository)
|
||||
|
||||
// init the workflow
|
||||
wfTreePath := ".gitea/workflows/pull.yml"
|
||||
wfFileContent := `name: Pull Request
|
||||
on:
|
||||
pull_request:
|
||||
types:
|
||||
- closed
|
||||
paths:
|
||||
- 'app/**'
|
||||
jobs:
|
||||
echo:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- run: echo 'Hello World'
|
||||
`
|
||||
|
||||
req = NewRequestWithJSON(t, "POST", fmt.Sprintf("/api/v1/repos/%s/%s/contents/%s", baseRepo.OwnerName, baseRepo.Name, wfTreePath), &api.CreateFileOptions{
|
||||
FileOptions: api.FileOptions{
|
||||
BranchName: baseRepo.DefaultBranch,
|
||||
Message: "create " + wfTreePath,
|
||||
Author: api.Identity{
|
||||
Name: user2.Name,
|
||||
Email: user2.Email,
|
||||
},
|
||||
Committer: api.Identity{
|
||||
Name: user2.Name,
|
||||
Email: user2.Email,
|
||||
},
|
||||
Dates: api.CommitDateOptions{
|
||||
Author: time.Now(),
|
||||
Committer: time.Now(),
|
||||
},
|
||||
},
|
||||
ContentBase64: base64.StdEncoding.EncodeToString([]byte(wfFileContent)),
|
||||
}).AddTokenAuth(user2Token)
|
||||
MakeRequest(t, req, http.StatusCreated)
|
||||
|
||||
// user4 forks the repo
|
||||
req = NewRequestWithJSON(t, "POST", fmt.Sprintf("/api/v1/repos/%s/%s/forks", baseRepo.OwnerName, baseRepo.Name),
|
||||
&api.CreateForkOption{
|
||||
Name: util.ToPointer("close-pull-request-with-path-fork"),
|
||||
}).AddTokenAuth(user4Token)
|
||||
resp = MakeRequest(t, req, http.StatusAccepted)
|
||||
var apiForkRepo api.Repository
|
||||
DecodeJSON(t, resp, &apiForkRepo)
|
||||
forkRepo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: apiForkRepo.ID})
|
||||
user4APICtx := NewAPITestContext(t, user4.Name, forkRepo.Name, auth_model.AccessTokenScopeWriteRepository)
|
||||
|
||||
// user4 creates a pull request to add file "app/main.go"
|
||||
doAPICreateFile(user4APICtx, "app/main.go", &api.CreateFileOptions{
|
||||
FileOptions: api.FileOptions{
|
||||
NewBranchName: "user4/add-main",
|
||||
Message: "create main.go",
|
||||
Author: api.Identity{
|
||||
Name: user4.Name,
|
||||
Email: user4.Email,
|
||||
},
|
||||
Committer: api.Identity{
|
||||
Name: user4.Name,
|
||||
Email: user4.Email,
|
||||
},
|
||||
Dates: api.CommitDateOptions{
|
||||
Author: time.Now(),
|
||||
Committer: time.Now(),
|
||||
},
|
||||
},
|
||||
ContentBase64: base64.StdEncoding.EncodeToString([]byte("// main.go")),
|
||||
})(t)
|
||||
apiPull, err := doAPICreatePullRequest(user4APICtx, baseRepo.OwnerName, baseRepo.Name, baseRepo.DefaultBranch, user4.Name+":user4/add-main")(t)
|
||||
assert.NoError(t, err)
|
||||
|
||||
doAPIMergePullRequest(user2APICtx, baseRepo.OwnerName, baseRepo.Name, apiPull.Index)(t)
|
||||
|
||||
pullRequest := unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{ID: apiPull.ID})
|
||||
|
||||
// load and compare ActionRun
|
||||
assert.Equal(t, 1, unittest.GetCount(t, &actions_model.ActionRun{RepoID: baseRepo.ID}))
|
||||
actionRun := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRun{RepoID: baseRepo.ID})
|
||||
assert.Equal(t, actions_module.GithubEventPullRequest, actionRun.TriggerEvent)
|
||||
assert.Equal(t, pullRequest.MergedCommitID, actionRun.CommitSHA)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -148,9 +148,9 @@ func TestAPIOrgEditBadVisibility(t *testing.T) {
|
||||
|
||||
func TestAPIOrgDeny(t *testing.T) {
|
||||
onGiteaRun(t, func(*testing.T, *url.URL) {
|
||||
setting.Service.RequireSignInView = true
|
||||
setting.Service.RequireSignInViewStrict = true
|
||||
defer func() {
|
||||
setting.Service.RequireSignInView = false
|
||||
setting.Service.RequireSignInViewStrict = false
|
||||
}()
|
||||
|
||||
orgName := "user1_org"
|
||||
|
||||
@@ -111,7 +111,7 @@ func TestPackageContainer(t *testing.T) {
|
||||
AddTokenAuth(anonymousToken)
|
||||
MakeRequest(t, req, http.StatusOK)
|
||||
|
||||
defer test.MockVariableValue(&setting.Service.RequireSignInView, true)()
|
||||
defer test.MockVariableValue(&setting.Service.RequireSignInViewStrict, true)()
|
||||
|
||||
req = NewRequest(t, "GET", fmt.Sprintf("%sv2", setting.AppURL))
|
||||
MakeRequest(t, req, http.StatusUnauthorized)
|
||||
|
||||
@@ -15,6 +15,7 @@ import (
|
||||
"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/test"
|
||||
"code.gitea.io/gitea/tests"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
@@ -131,11 +132,7 @@ func TestPackageGeneric(t *testing.T) {
|
||||
|
||||
t.Run("RequireSignInView", func(t *testing.T) {
|
||||
defer tests.PrintCurrentTest(t)()
|
||||
|
||||
setting.Service.RequireSignInView = true
|
||||
defer func() {
|
||||
setting.Service.RequireSignInView = false
|
||||
}()
|
||||
defer test.MockVariableValue(&setting.Service.RequireSignInViewStrict, true)()
|
||||
|
||||
req = NewRequest(t, "GET", url+"/dummy.bin")
|
||||
MakeRequest(t, req, http.StatusUnauthorized)
|
||||
|
||||
@@ -9,11 +9,17 @@ import (
|
||||
"net/url"
|
||||
"testing"
|
||||
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/modules/test"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestGitSmartHTTP(t *testing.T) {
|
||||
onGiteaRun(t, testGitSmartHTTP)
|
||||
onGiteaRun(t, func(t *testing.T, u *url.URL) {
|
||||
testGitSmartHTTP(t, u)
|
||||
testRenamedRepoRedirect(t)
|
||||
})
|
||||
}
|
||||
|
||||
func testGitSmartHTTP(t *testing.T, u *url.URL) {
|
||||
@@ -66,3 +72,21 @@ func testGitSmartHTTP(t *testing.T, u *url.URL) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func testRenamedRepoRedirect(t *testing.T) {
|
||||
defer test.MockVariableValue(&setting.Service.RequireSignInViewStrict, true)()
|
||||
|
||||
// git client requires to get a 301 redirect response before 401 unauthorized response
|
||||
req := NewRequest(t, "GET", "/user2/oldrepo1/info/refs")
|
||||
resp := MakeRequest(t, req, http.StatusMovedPermanently)
|
||||
redirect := resp.Header().Get("Location")
|
||||
assert.Equal(t, "/user2/repo1/info/refs", redirect)
|
||||
|
||||
req = NewRequest(t, "GET", redirect)
|
||||
resp = MakeRequest(t, req, http.StatusUnauthorized)
|
||||
assert.Equal(t, "Unauthorized\n", resp.Body.String())
|
||||
|
||||
req = NewRequest(t, "GET", redirect).AddBasicAuth("user2")
|
||||
resp = MakeRequest(t, req, http.StatusOK)
|
||||
assert.Contains(t, resp.Body.String(), "65f1bf27bc3bf70f64657658635e66094edbcb4d\trefs/tags/v1.1")
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
"path"
|
||||
"sort"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
@@ -67,7 +68,7 @@ func TestPullView_CodeOwner(t *testing.T) {
|
||||
{
|
||||
Operation: "create",
|
||||
TreePath: "CODEOWNERS",
|
||||
ContentReader: strings.NewReader("README.md @user5\n"),
|
||||
ContentReader: strings.NewReader("README.md @user5\nuser8-file.md @user8\n"),
|
||||
},
|
||||
},
|
||||
})
|
||||
@@ -75,7 +76,7 @@ func TestPullView_CodeOwner(t *testing.T) {
|
||||
|
||||
t.Run("First Pull Request", func(t *testing.T) {
|
||||
// create a new branch to prepare for pull request
|
||||
_, err = files_service.ChangeRepoFiles(db.DefaultContext, repo, user2, &files_service.ChangeRepoFilesOptions{
|
||||
resp1, err := files_service.ChangeRepoFiles(db.DefaultContext, repo, user2, &files_service.ChangeRepoFilesOptions{
|
||||
NewBranch: "codeowner-basebranch",
|
||||
Files: []*files_service.ChangeRepoFile{
|
||||
{
|
||||
@@ -95,7 +96,37 @@ func TestPullView_CodeOwner(t *testing.T) {
|
||||
unittest.AssertExistsIf(t, true, &issues_model.Review{IssueID: pr.IssueID, Type: issues_model.ReviewTypeRequest, ReviewerID: 5})
|
||||
assert.NoError(t, pr.LoadIssue(db.DefaultContext))
|
||||
|
||||
err := issue_service.ChangeTitle(db.DefaultContext, pr.Issue, user2, "[WIP] Test Pull Request")
|
||||
reviewNotifiers, err := issue_service.PullRequestCodeOwnersReview(db.DefaultContext, pr)
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, reviewNotifiers, 1)
|
||||
assert.EqualValues(t, 5, reviewNotifiers[0].Reviewer.ID)
|
||||
|
||||
// update the file on the pr branch
|
||||
resp2, err := files_service.ChangeRepoFiles(db.DefaultContext, repo, user2, &files_service.ChangeRepoFilesOptions{
|
||||
OldBranch: "codeowner-basebranch",
|
||||
Files: []*files_service.ChangeRepoFile{
|
||||
{
|
||||
Operation: "create",
|
||||
TreePath: "user8-file.md",
|
||||
ContentReader: strings.NewReader("# This is a new project2\n"),
|
||||
},
|
||||
},
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
|
||||
reviewNotifiers, err = issue_service.PullRequestCodeOwnersReview(db.DefaultContext, pr)
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, reviewNotifiers, 2)
|
||||
reviewerIDs := []int64{reviewNotifiers[0].Reviewer.ID, reviewNotifiers[1].Reviewer.ID}
|
||||
sort.Slice(reviewerIDs, func(i, j int) bool { return reviewerIDs[i] < reviewerIDs[j] })
|
||||
assert.EqualValues(t, []int64{5, 8}, reviewerIDs)
|
||||
|
||||
reviewNotifiers, err = issue_service.PullRequestCodeOwnersReviewSpecialCommits(db.DefaultContext, pr, resp1.Commit.SHA, resp2.Commit.SHA)
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, reviewNotifiers, 1)
|
||||
assert.EqualValues(t, 8, reviewNotifiers[0].Reviewer.ID)
|
||||
|
||||
err = issue_service.ChangeTitle(db.DefaultContext, pr.Issue, user2, "[WIP] Test Pull Request")
|
||||
assert.NoError(t, err)
|
||||
prUpdated1 := unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{ID: pr.ID})
|
||||
assert.NoError(t, prUpdated1.LoadIssue(db.DefaultContext))
|
||||
@@ -139,7 +170,12 @@ func TestPullView_CodeOwner(t *testing.T) {
|
||||
testPullCreate(t, session, "user2", "test_codeowner", false, repo.DefaultBranch, "codeowner-basebranch2", "Test Pull Request2")
|
||||
|
||||
pr := unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{BaseRepoID: repo.ID, HeadBranch: "codeowner-basebranch2"})
|
||||
unittest.AssertExistsIf(t, true, &issues_model.Review{IssueID: pr.IssueID, Type: issues_model.ReviewTypeRequest, ReviewerID: 8})
|
||||
unittest.AssertExistsAndLoadBean(t, &issues_model.Review{IssueID: pr.IssueID, Type: issues_model.ReviewTypeRequest, ReviewerID: 8})
|
||||
|
||||
reviewNotifiers, err := issue_service.PullRequestCodeOwnersReview(db.DefaultContext, pr)
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, reviewNotifiers, 1)
|
||||
assert.EqualValues(t, 8, reviewNotifiers[0].Reviewer.ID)
|
||||
})
|
||||
|
||||
t.Run("Forked Repo Pull Request", func(t *testing.T) {
|
||||
|
||||
@@ -4,12 +4,15 @@
|
||||
package integration
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"code.gitea.io/gitea/models/db"
|
||||
repo_model "code.gitea.io/gitea/models/repo"
|
||||
"code.gitea.io/gitea/models/unittest"
|
||||
"code.gitea.io/gitea/modules/test"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
@@ -61,5 +64,14 @@ func TestRepoActivity(t *testing.T) {
|
||||
// Should be 3 new issues
|
||||
list = htmlDoc.doc.Find("#new-issues").Next().Find("p.desc")
|
||||
assert.Len(t, list.Nodes, 3)
|
||||
|
||||
// Non-existing default branch
|
||||
repo1 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{Name: "repo1"})
|
||||
repo1.DefaultBranch = "no-such-branch"
|
||||
_, _ = db.GetEngine(context.Background()).Cols("default_branch").Update(repo1)
|
||||
req = NewRequest(t, "GET", "/user2/repo1/activity")
|
||||
req.Header.Add("Accept", "text/html")
|
||||
resp = session.MakeRequest(t, req, http.StatusNotFound)
|
||||
assert.Contains(t, resp.Body.String(), `Default branch "no-such-branch" does not exist.`)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -15,11 +15,13 @@ import (
|
||||
"code.gitea.io/gitea/modules/test"
|
||||
"code.gitea.io/gitea/modules/translation"
|
||||
"code.gitea.io/gitea/modules/web"
|
||||
"code.gitea.io/gitea/routers"
|
||||
"code.gitea.io/gitea/services/context"
|
||||
"code.gitea.io/gitea/tests"
|
||||
|
||||
"github.com/markbates/goth"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func testLoginFailed(t *testing.T, username, password, message string) {
|
||||
@@ -158,3 +160,32 @@ func TestEnablePasswordSignInFormAndEnablePasskeyAuth(t *testing.T) {
|
||||
NewHTMLParser(t, resp.Body).AssertElement(t, ".signin-passkey", true)
|
||||
})
|
||||
}
|
||||
|
||||
func TestRequireSignInView(t *testing.T) {
|
||||
defer tests.PrepareTestEnv(t)()
|
||||
t.Run("NoRequireSignInView", func(t *testing.T) {
|
||||
require.False(t, setting.Service.RequireSignInViewStrict)
|
||||
require.False(t, setting.Service.BlockAnonymousAccessExpensive)
|
||||
req := NewRequest(t, "GET", "/user2/repo1/src/branch/master")
|
||||
MakeRequest(t, req, http.StatusOK)
|
||||
})
|
||||
t.Run("RequireSignInView", func(t *testing.T) {
|
||||
defer test.MockVariableValue(&setting.Service.RequireSignInViewStrict, true)()
|
||||
defer test.MockVariableValue(&testWebRoutes, routers.NormalRoutes())()
|
||||
req := NewRequest(t, "GET", "/user2/repo1/src/branch/master")
|
||||
resp := MakeRequest(t, req, http.StatusSeeOther)
|
||||
assert.Equal(t, "/user/login", resp.Header().Get("Location"))
|
||||
})
|
||||
t.Run("BlockAnonymousAccessExpensive", func(t *testing.T) {
|
||||
defer test.MockVariableValue(&setting.Service.RequireSignInViewStrict, false)()
|
||||
defer test.MockVariableValue(&setting.Service.BlockAnonymousAccessExpensive, true)()
|
||||
defer test.MockVariableValue(&testWebRoutes, routers.NormalRoutes())()
|
||||
|
||||
req := NewRequest(t, "GET", "/user2/repo1")
|
||||
MakeRequest(t, req, http.StatusOK)
|
||||
|
||||
req = NewRequest(t, "GET", "/user2/repo1/src/branch/master")
|
||||
resp := MakeRequest(t, req, http.StatusSeeOther)
|
||||
assert.Equal(t, "/user/login", resp.Header().Get("Location"))
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1225,14 +1225,7 @@ table th[data-sortt-desc] .svg {
|
||||
box-shadow: 0 0 0 1px var(--color-secondary) inset;
|
||||
}
|
||||
|
||||
.emoji {
|
||||
font-size: 1.25em;
|
||||
line-height: var(--line-height-default);
|
||||
font-style: normal !important;
|
||||
font-weight: var(--font-weight-normal) !important;
|
||||
vertical-align: -0.075em;
|
||||
}
|
||||
|
||||
/* for "image" emojis like ":git:" ":gitea:" and ":github:" (see CUSTOM_EMOJIS config option) */
|
||||
.emoji img {
|
||||
border-width: 0 !important;
|
||||
margin: 0 !important;
|
||||
@@ -1391,6 +1384,11 @@ table th[data-sortt-desc] .svg {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.flex-text-block > .ui.button,
|
||||
.flex-text-inline > .ui.button {
|
||||
margin: 0; /* fomantic buttons have default margin, when we use them in a flex container with gap, we do not need these margins */
|
||||
}
|
||||
|
||||
/* to override Fomantic's default display: block for ".menu .item", and use a slightly larger gap for menu item content
|
||||
the "!important" is necessary to override Fomantic UI menu item styles, meanwhile we should keep the "hidden" items still hidden */
|
||||
.ui.dropdown .menu.flex-items-menu > .item:not(.hidden, .filtered, .tw-hidden) {
|
||||
|
||||
@@ -8,18 +8,6 @@
|
||||
margin: 0 0.5em;
|
||||
}
|
||||
|
||||
.project-toolbar-right .filter.menu {
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
@media (max-width: 767.98px) {
|
||||
.project-toolbar-right .dropdown .menu {
|
||||
left: auto !important;
|
||||
right: auto !important;
|
||||
}
|
||||
}
|
||||
|
||||
.project-column {
|
||||
background-color: var(--color-project-column-bg) !important;
|
||||
border: 1px solid var(--color-secondary) !important;
|
||||
|
||||
@@ -337,11 +337,6 @@
|
||||
padding-right: 28px;
|
||||
}
|
||||
|
||||
.markup .emoji {
|
||||
max-width: none;
|
||||
vertical-align: text-top;
|
||||
}
|
||||
|
||||
.markup span.frame {
|
||||
display: block;
|
||||
overflow: hidden;
|
||||
@@ -521,6 +516,18 @@
|
||||
padding-left: 2em;
|
||||
}
|
||||
|
||||
.markup details.frontmatter-content summary {
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
margin-bottom: 0.25em;
|
||||
}
|
||||
|
||||
.markup details.frontmatter-content svg {
|
||||
vertical-align: middle;
|
||||
margin: 0 0.25em;
|
||||
}
|
||||
|
||||
.file-revisions-btn {
|
||||
display: block;
|
||||
float: left;
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
import $ from 'jquery';
|
||||
import {htmlEscape} from 'escape-goat';
|
||||
import {hideElem, showElem} from '../utils/dom.ts';
|
||||
import {hideElem, querySingleVisibleElem, showElem, toggleElem} from '../utils/dom.ts';
|
||||
|
||||
const {appSubUrl} = window.config;
|
||||
|
||||
export function initRepoTemplateSearch() {
|
||||
const $repoTemplate = $('#repo_template');
|
||||
if (!$repoTemplate.length) return; // make sure the current page is "new repo" page
|
||||
|
||||
const checkTemplate = function () {
|
||||
const $templateUnits = $('#template_units');
|
||||
const $nonTemplate = $('#non_template');
|
||||
@@ -21,6 +23,20 @@ export function initRepoTemplateSearch() {
|
||||
checkTemplate();
|
||||
|
||||
const changeOwner = function () {
|
||||
const elUid = document.querySelector<HTMLInputElement>('#uid');
|
||||
const elForm = elUid.closest('form');
|
||||
const elSubmitButton = querySingleVisibleElem<HTMLInputElement>(elForm, '.ui.primary.button');
|
||||
const elCreateRepoErrorMessage = elForm.querySelector('#create-repo-error-message');
|
||||
const elOwnerItem = document.querySelector(`.ui.selection.owner.dropdown .menu > .item[data-value="${CSS.escape(elUid.value)}"]`);
|
||||
hideElem(elCreateRepoErrorMessage);
|
||||
elSubmitButton.disabled = false;
|
||||
if (elOwnerItem) {
|
||||
elCreateRepoErrorMessage.textContent = elOwnerItem.getAttribute('data-create-repo-disallowed-prompt') ?? '';
|
||||
const hasError = Boolean(elCreateRepoErrorMessage.textContent);
|
||||
toggleElem(elCreateRepoErrorMessage, hasError);
|
||||
elSubmitButton.disabled = hasError;
|
||||
}
|
||||
|
||||
$('#repo_template_search')
|
||||
.dropdown({
|
||||
apiSettings: {
|
||||
|
||||
7
web_src/js/webcomponents/polyfill.test.ts
Normal file
7
web_src/js/webcomponents/polyfill.test.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import {weakRefClass} from './polyfills.ts';
|
||||
|
||||
test('polyfillWeakRef', () => {
|
||||
const WeakRef = weakRefClass();
|
||||
const r = new WeakRef(123);
|
||||
expect(r.deref()).toEqual(123);
|
||||
});
|
||||
@@ -16,3 +16,19 @@ try {
|
||||
return intlNumberFormat(locales, options);
|
||||
};
|
||||
}
|
||||
|
||||
export function weakRefClass() {
|
||||
const weakMap = new WeakMap();
|
||||
return class {
|
||||
constructor(target: any) {
|
||||
weakMap.set(this, target);
|
||||
}
|
||||
deref() {
|
||||
return weakMap.get(this);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
if (!window.WeakRef) {
|
||||
window.WeakRef = weakRefClass() as any;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user