Compare commits

...

23 Commits

Author SHA1 Message Date
Lunny Xiao
97171be1b4 Release note 1.23.7 (#34117) 2025-04-07 18:31:28 +00:00
wxiaoguang
9ef2a338d8 Fix block expensive for 1.23 (#34127) 2025-04-06 21:50:37 +08:00
Giteabot
66ccae8b90 Fix discord webhook 400 status code when description limit is exceeded (#34084) (#34124)
Backport #34084 by @Mopcho

Fixes [#34027](https://github.com/go-gitea/gitea/issues/34027)

Discord does not allow for description bigger than 2048 bytes. If the
description is bigger than that it will throw 400 and the event won't
appear in discord. To fix that, in the createPayload method we now slice
the description to ensure it doesn’t exceed the limit.

---------

Co-authored-by: Mopcho <47301161+Mopcho@users.noreply.github.com>
Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
Co-authored-by: Lunny Xiao <xiaolunwen@gmail.com>
2025-04-05 02:08:04 +00:00
Zettat123
0e6489317e Get changed files based on merge base when checking pull_request actions trigger (#34106) (#34120)
Backport #34106

Fix #33941
2025-04-03 22:10:54 -07:00
Giteabot
67dc1ff926 Fix invalid version in RPM package path (#34112) (#34115)
Backport #34112 by @KN4CK3R

Fixes #34017

Co-authored-by: KN4CK3R <admin@oldschoolhack.me>
2025-04-03 21:18:46 +00:00
Giteabot
4ee4c06b07 also check default ssh-cert location for host (#34099) (#34100) (#34116)
Backport #34100 by @ManInDark

Fixes #34099.

Resolved by checking the `key-cert.pub` location alongside the
previously configured location. In case a certificate is already found,
this won't change anything, but if there is one in `key-cert.pub` but
not in `key_cert`, it'll use that one now.

Co-authored-by: ManInDark <61268856+ManInDark@users.noreply.github.com>
2025-04-04 04:38:24 +08:00
wxiaoguang
3063e37802 Fix markdown frontmatter rendering (#34102) (#34107)
Backport #34102
Fix #34101
2025-04-03 15:26:43 +08:00
wxiaoguang
a40e15a116 Add new CLI flags to set name and scopes when creating a user with access token (#34080) (#34103)
Backport #34080

Co-authored-by: Kemal Zebari <60799661+kemzeb@users.noreply.github.com>
2025-04-03 01:05:28 +08:00
wxiaoguang
8f75f61b64 Do not show 500 error when default branch doesn't exist (#34096) (#34097)
Backport #34096
2025-04-02 18:16:41 +08:00
Giteabot
25e409e025 Return default avatar url when user id is zero rather than updating database (#34094) (#34095)
Backport #34094 by @lunny

When visit commit list, it would update the user avatar even if id = 0,
which was unnecessary operations. This PR returned default avatar for
the git only user avatar rendering who's user id is zero.

```log
database duration=0.0005s db.sql="UPDATE `user` SET `avatar` = ?, `updated_unix` = ? WHERE `id`=?"
database duration=0.0007s db.sql="UPDATE `user` SET `avatar` = ?, `updated_unix` = ? WHERE `id`=?"
...
```

Co-authored-by: Lunny Xiao <xiaolunwen@gmail.com>
2025-04-02 09:37:17 +08:00
Lunny Xiao
f8f24d83cf Hide activity contributors, recent commits and code frequrency left tabs if there is no code permission (#34053) (#34065)
Backport #34053 

When a team have no code unit permission of a repository, the member of
the team should not view activity contributors, recent commits and code
frequrency.

---------

Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
2025-03-30 18:30:55 +08:00
wxiaoguang
15e93a751c Add a config option to block "expensive" pages for anonymous users (#34024) (#34071)
Backport #34024 since there are too many AI crawlers. The new code is
covered by tests and it does nothing if users don't set it.
2025-03-30 06:16:32 +00:00
Giteabot
5a9b3bfa50 add additional ReplaceAll in pathsep to cater for different pathsep (#34061) (#34070)
Backport #34061 by eeyrjmr

The doctor storage check reconstructs the lfs oid by producing a string
where the path separator is stripped
ab/dc/efg -> abdcefg. Windows however uses a backslash and thus the
ReplaceAll call doesn't produce the correct oid resulting in all lfs
objects being classed as orphaned.
This PR allows this to be more OS agnostic.

Closes #34039

Co-authored-by: JonRB <4564448+eeyrjmr@users.noreply.github.com>
2025-03-30 05:51:08 +00:00
wxiaoguang
dd901983c0 Fix repo-template.ts error in 1.23 (#34060)
Fix #34059
2025-03-29 12:30:27 -07:00
Lunny Xiao
7f962a16c9 Pull request updates will also trigger code owners review requests (#33744) (#34045)
Fix #33490
Backport #33744 

It will only read the changed file on the pushed commits but not all the
files of this PR.

Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
2025-03-28 17:24:31 +00:00
wxiaoguang
5d81f6d73f Adjust the layout of the toolbar on the Issues/Projects page (#33667) (#34047)
And fix layout for mobile

Backport #33667 

Fix #33880

---------

Co-authored-by: Kerwin Bryant <kerwin612@qq.com>
2025-03-28 12:37:27 +00:00
Giteabot
eee4a752a5 Simplify emoji rendering (#34048) (#34049)
Backport #34048 by silverwind

Co-authored-by: silverwind <me@silverwind.io>
Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
2025-03-28 10:30:29 +00:00
wxiaoguang
b3516767fb fix org repo creation being limited by user limits (#34030) (#34044)
Backport #34030
2025-03-28 07:59:46 +00:00
wxiaoguang
8d1be2a9c5 Fix git client accessing renamed repo (#34034) (#34043)
Backport #34034
2025-03-28 14:59:46 +08:00
Giteabot
e46f9ff534 Fix the issue with error message logging for the check-attr command on Windows OS. (#34035) (#34036)
Backport #34035 by charles7668

Close #34022 , #33550 

This error message always appears when using the `check-attr` command,
even though it works correctly.
The issue occurs when the stdin writer is closed, so I added a special
case to handle and check the error message when the exit code is 1.

Co-authored-by: charles <30816317+charles7668@users.noreply.github.com>
2025-03-27 09:50:57 +00:00
Giteabot
690e810bcc Try to fix check-attr bug (#34029) (#34033)
Backport #34029 by wxiaoguang

Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
2025-03-27 05:36:07 +00:00
Giteabot
35983ac0a8 Polyfill WeakRef (#34025) (#34028)
Backport #34025 by wxiaoguang

Fix #33407

Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
2025-03-27 00:13:22 +08:00
Giteabot
4b3400bd9c Git client will follow 301 but 307 (#34005) (#34010)
Backport #34005 by lunny

Fix #28460

Co-authored-by: Lunny Xiao <xiaolunwen@gmail.com>
Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
2025-03-25 15:56:44 +08:00
71 changed files with 989 additions and 335 deletions

View File

@@ -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 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). 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 ## [1.23.6](https://github.com/go-gitea/gitea/releases/tag/v1.23.6) - 2025-03-24
* SECURITY * SECURITY

View File

@@ -7,6 +7,7 @@ import (
"context" "context"
"errors" "errors"
"fmt" "fmt"
"strings"
auth_model "code.gitea.io/gitea/models/auth" auth_model "code.gitea.io/gitea/models/auth"
"code.gitea.io/gitea/models/db" "code.gitea.io/gitea/models/db"
@@ -61,6 +62,16 @@ var microcmdUserCreate = &cli.Command{
Name: "access-token", Name: "access-token",
Usage: "Generate access token for the user", 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{ &cli.BoolFlag{
Name: "restricted", Name: "restricted",
Usage: "Make a restricted user account", Usage: "Make a restricted user account",
@@ -162,23 +173,39 @@ func runCreateUser(c *cli.Context) error {
IsRestricted: restricted, 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 { if err := user_model.CreateUser(ctx, u, &user_model.Meta{}, overwriteDefault); err != nil {
return fmt.Errorf("CreateUser: %w", err) return fmt.Errorf("CreateUser: %w", err)
} }
// create the access token
if c.Bool("access-token") { if accessTokenScope != "" {
t := &auth_model.AccessToken{ t := &auth_model.AccessToken{Name: accessTokenName, UID: u.ID, Scope: accessTokenScope}
Name: "gitea-admin",
UID: u.ID,
}
if err := auth_model.NewAccessToken(ctx, t); err != nil { if err := auth_model.NewAccessToken(ctx, t); err != nil {
return err return err
} }
fmt.Printf("Access token was successfully created... %s\n", t.Token) fmt.Printf("Access token was successfully created... %s\n", t.Token)
} }
fmt.Printf("New user '%s' has been successfully created!\n", username) fmt.Printf("New user '%s' has been successfully created!\n", username)
return nil return nil
} }

View File

@@ -8,37 +8,97 @@ import (
"strings" "strings"
"testing" "testing"
auth_model "code.gitea.io/gitea/models/auth"
"code.gitea.io/gitea/models/db" "code.gitea.io/gitea/models/db"
"code.gitea.io/gitea/models/unittest" "code.gitea.io/gitea/models/unittest"
user_model "code.gitea.io/gitea/models/user" user_model "code.gitea.io/gitea/models/user"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
) )
func TestAdminUserCreate(t *testing.T) { func TestAdminUserCreate(t *testing.T) {
app := NewMainApp(AppVersion{}) app := NewMainApp(AppVersion{})
reset := func() { reset := func() {
assert.NoError(t, db.TruncateBeans(db.DefaultContext, &user_model.User{})) require.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.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 } t.Run("AccessToken", func(t *testing.T) {
createUser := func(name, args string) createCheck { // no generated access token
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)))) reset()
u := unittest.AssertExistsAndLoadBean(t, &user_model.User{LowerName: name}) assert.NoError(t, createUser("u", "--random-password"))
return createCheck{u.IsAdmin, u.MustChangePassword} 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: false, MustChangePassword: false}, createUser("u", ""), "first non-admin user doesn't need to change password")
reset() // using "--access-token" only means "all" access
assert.Equal(t, createCheck{IsAdmin: true, MustChangePassword: false}, createUser("u", "--admin"), "first admin user doesn't need to change password") 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() // using "--access-token" with name & scopes
assert.Equal(t, createCheck{IsAdmin: true, MustChangePassword: true}, createUser("u", "--admin --must-change-password")) reset()
assert.Equal(t, createCheck{IsAdmin: true, MustChangePassword: true}, createUser("u2", "--admin")) 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, createCheck{IsAdmin: true, MustChangePassword: false}, createUser("u3", "--admin --must-change-password=false")) assert.Equal(t, 1, unittest.GetCount(t, &user_model.User{}))
assert.Equal(t, createCheck{IsAdmin: false, MustChangePassword: true}, createUser("u4", "")) assert.Equal(t, 1, unittest.GetCount(t, &auth_model.AccessToken{}))
assert.Equal(t, createCheck{IsAdmin: false, MustChangePassword: false}, createUser("u5", "--must-change-password=false")) 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")
})
} }

View File

@@ -34,8 +34,8 @@ var microcmdUserGenerateAccessToken = &cli.Command{
}, },
&cli.StringFlag{ &cli.StringFlag{
Name: "scopes", Name: "scopes",
Value: "", Value: "all",
Usage: "Comma separated list of scopes to apply to access token", Usage: `Comma separated list of scopes to apply to access token, examples: "all", "public-only,read:issue", "write:repository,write:user"`,
}, },
}, },
Action: runGenerateAccessToken, Action: runGenerateAccessToken,
@@ -43,7 +43,7 @@ var microcmdUserGenerateAccessToken = &cli.Command{
func runGenerateAccessToken(c *cli.Context) error { func runGenerateAccessToken(c *cli.Context) error {
if !c.IsSet("username") { 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() ctx, cancel := installSignals()
@@ -77,6 +77,9 @@ func runGenerateAccessToken(c *cli.Context) error {
if err != nil { if err != nil {
return fmt.Errorf("invalid access token scope provided: %w", err) 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 t.Scope = accessTokenScope
// create the token // create the token

View File

@@ -774,6 +774,9 @@ LEVEL = Info
;ALLOW_ONLY_EXTERNAL_REGISTRATION = false ;ALLOW_ONLY_EXTERNAL_REGISTRATION = false
;; ;;
;; User must sign in to view anything. ;; 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 ;REQUIRE_SIGNIN_VIEW = false
;; ;;
;; Mail notification ;; Mail notification

View File

@@ -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"} SSH_ECDSA_CERT=${SSH_ECDSA_CERT:-"/data/ssh/ssh_host_ecdsa_cert"}
fi 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 if [ -d /etc/ssh ]; then
SSH_PORT=${SSH_PORT:-"22"} \ SSH_PORT=${SSH_PORT:-"22"} \
SSH_LISTEN_PORT=${SSH_LISTEN_PORT:-"${SSH_PORT}"} \ SSH_LISTEN_PORT=${SSH_LISTEN_PORT:-"${SSH_PORT}"} \

View File

@@ -283,6 +283,10 @@ func (s AccessTokenScope) Normalize() (AccessTokenScope, error) {
return bitmap.toScope(), nil return bitmap.toScope(), nil
} }
func (s AccessTokenScope) HasPermissionScope() bool {
return s != "" && s != AccessTokenScopePublicOnly
}
// PublicOnly checks if this token scope is limited to public resources // PublicOnly checks if this token scope is limited to public resources
func (s AccessTokenScope) PublicOnly() (bool, error) { func (s AccessTokenScope) PublicOnly() (bool, error) {
bitmap, err := s.parse() bitmap, err := s.parse()

View File

@@ -173,6 +173,18 @@ func GetBranch(ctx context.Context, repoID int64, branchName string) (*Branch, e
return &branch, nil 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) { func GetBranches(ctx context.Context, repoID int64, branchNames []string, includeDeleted bool) ([]*Branch, error) {
branches := make([]*Branch, 0, len(branchNames)) branches := make([]*Branch, 0, len(branchNames))

View File

@@ -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 // 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 { 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() return avatars.DefaultAvatarLink()
} }

View File

@@ -463,7 +463,7 @@ func matchPullRequestEvent(gitRepo *git.Repository, commit *git.Commit, prPayloa
matchTimes++ matchTimes++
} }
case "paths": case "paths":
filesChanged, err := headCommit.GetFilesChangedSinceCommit(prPayload.PullRequest.Base.Ref) filesChanged, err := headCommit.GetFilesChangedSinceCommit(prPayload.PullRequest.MergeBase)
if err != nil { if err != nil {
log.Error("GetFilesChangedSinceCommit [commit_sha1: %s]: %v", headCommit.ID.String(), err) log.Error("GetFilesChangedSinceCommit [commit_sha1: %s]: %v", headCommit.ID.String(), err)
} else { } else {
@@ -476,7 +476,7 @@ func matchPullRequestEvent(gitRepo *git.Repository, commit *git.Commit, prPayloa
} }
} }
case "paths-ignore": case "paths-ignore":
filesChanged, err := headCommit.GetFilesChangedSinceCommit(prPayload.PullRequest.Base.Ref) filesChanged, err := headCommit.GetFilesChangedSinceCommit(prPayload.PullRequest.MergeBase)
if err != nil { if err != nil {
log.Error("GetFilesChangedSinceCommit [commit_sha1: %s]: %v", headCommit.ID.String(), err) log.Error("GetFilesChangedSinceCommit [commit_sha1: %s]: %v", headCommit.ID.String(), err)
} else { } else {

View File

@@ -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. // 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. // 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. // 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" && if runtime.GOOS == "windows" &&
err != nil && err != nil &&
err.Error() == "" && (err.Error() == "" || err.Error() == "exit status 1") &&
cmd.ProcessState.ExitCode() == 1 && cmd.ProcessState.ExitCode() == 1 &&
ctx.Err() == context.Canceled { ctx.Err() == context.Canceled {
return ctx.Err() return ctx.Err()

View File

@@ -280,7 +280,7 @@ func (wr *nulSeparatedAttributeWriter) Write(p []byte) (n int, err error) {
} }
} }
wr.tmp = append(wr.tmp, p...) wr.tmp = append(wr.tmp, p...)
return len(p), nil return l, nil
} }
func (wr *nulSeparatedAttributeWriter) ReadAttribute() <-chan attributeTriple { func (wr *nulSeparatedAttributeWriter) ReadAttribute() <-chan attributeTriple {

View File

@@ -4,6 +4,7 @@
package markdown package markdown
import ( import (
"html/template"
"strconv" "strconv"
"github.com/yuin/goldmark/ast" "github.com/yuin/goldmark/ast"
@@ -29,9 +30,7 @@ func (n *Details) Kind() ast.NodeKind {
// NewDetails returns a new Paragraph node. // NewDetails returns a new Paragraph node.
func NewDetails() *Details { func NewDetails() *Details {
return &Details{ return &Details{}
BaseBlock: ast.BaseBlock{},
}
} }
// Summary is a block that contains the summary of details block // 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. // NewSummary returns a new Summary node.
func NewSummary() *Summary { func NewSummary() *Summary {
return &Summary{ return &Summary{}
BaseBlock: ast.BaseBlock{},
}
} }
// TaskCheckBoxListItem is a block that represents a list item of a markdown block with a checkbox // 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 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 // ColorPreview is an inline for a color preview
type ColorPreview struct { type ColorPreview struct {
ast.BaseInline ast.BaseInline
@@ -175,3 +149,24 @@ func NewAttention(attentionType string) *Attention {
AttentionType: attentionType, 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}
}

View File

@@ -4,23 +4,22 @@
package markdown package markdown
import ( import (
"strings"
"code.gitea.io/gitea/modules/htmlutil"
"code.gitea.io/gitea/modules/svg"
"github.com/yuin/goldmark/ast" "github.com/yuin/goldmark/ast"
east "github.com/yuin/goldmark/extension/ast" east "github.com/yuin/goldmark/extension/ast"
"gopkg.in/yaml.v3" "gopkg.in/yaml.v3"
) )
func nodeToTable(meta *yaml.Node) ast.Node { func nodeToTable(meta *yaml.Node) ast.Node {
for { for meta != nil && meta.Kind == yaml.DocumentNode {
if meta == nil { meta = meta.Content[0]
return nil }
} if meta == nil {
switch meta.Kind { return nil
case yaml.DocumentNode:
meta = meta.Content[0]
continue
default:
}
break
} }
switch meta.Kind { switch meta.Kind {
case yaml.MappingNode: case yaml.MappingNode:
@@ -72,12 +71,28 @@ func sequenceNodeToTable(meta *yaml.Node) ast.Node {
return table 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 := NewDetails()
details.SetAttributeString(g.renderInternal.SafeAttr("class"), g.renderInternal.SafeValue("frontmatter-content"))
summary := NewSummary() 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, summary)
details.AppendChild(details, nodeToTable(meta)) details.AppendChild(details, nodeToTable(meta))
return details return details
} }

View File

@@ -5,9 +5,6 @@ package markdown
import ( import (
"fmt" "fmt"
"regexp"
"strings"
"sync"
"code.gitea.io/gitea/modules/container" "code.gitea.io/gitea/modules/container"
"code.gitea.io/gitea/modules/markup" "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) tocList := make([]Header, 0, 20)
if rc.yamlNode != nil { if rc.yamlNode != nil {
metaNode := rc.toMetaNode() metaNode := rc.toMetaNode(g)
if metaNode != nil { if metaNode != nil {
node.InsertBefore(node, firstChild, metaNode) 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. // NewHTMLRenderer creates a HTMLRenderer to render in the gitea form.
func NewHTMLRenderer(renderInternal *internal.RenderInternal, opts ...html.Option) renderer.NodeRenderer { func NewHTMLRenderer(renderInternal *internal.RenderInternal, opts ...html.Option) renderer.NodeRenderer {
r := &HTMLRenderer{ r := &HTMLRenderer{
@@ -140,11 +132,11 @@ func (r *HTMLRenderer) RegisterFuncs(reg renderer.NodeRendererFuncRegisterer) {
reg.Register(ast.KindDocument, r.renderDocument) reg.Register(ast.KindDocument, r.renderDocument)
reg.Register(KindDetails, r.renderDetails) reg.Register(KindDetails, r.renderDetails)
reg.Register(KindSummary, r.renderSummary) reg.Register(KindSummary, r.renderSummary)
reg.Register(KindIcon, r.renderIcon)
reg.Register(ast.KindCodeSpan, r.renderCodeSpan) reg.Register(ast.KindCodeSpan, r.renderCodeSpan)
reg.Register(KindAttention, r.renderAttention) reg.Register(KindAttention, r.renderAttention)
reg.Register(KindTaskCheckBoxListItem, r.renderTaskCheckBoxListItem) reg.Register(KindTaskCheckBoxListItem, r.renderTaskCheckBoxListItem)
reg.Register(east.KindTaskCheckBox, r.renderTaskCheckBox) 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) { 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 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 { if !entering {
return ast.WalkContinue, nil return ast.WalkContinue, nil
} }
n := node.(*RawHTML)
n := node.(*Icon) _, err := w.WriteString(string(r.renderInternal.ProtectSafeAttrs(n.rawHTML)))
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)
if err != nil { if err != nil {
return ast.WalkStop, err return ast.WalkStop, err
} }
return ast.WalkContinue, nil return ast.WalkContinue, nil
} }

View File

@@ -184,11 +184,7 @@ func render(ctx *markup.RenderContext, input io.Reader, output io.Writer) error
// Preserve original length. // Preserve original length.
bufWithMetadataLength := len(buf) bufWithMetadataLength := len(buf)
rc := &RenderConfig{ rc := &RenderConfig{Meta: markup.RenderMetaAsDetails}
Meta: markup.RenderMetaAsDetails,
Icon: "table",
Lang: "",
}
buf, _ = ExtractMetadataBytes(buf, rc) buf, _ = ExtractMetadataBytes(buf, rc)
metaLength := bufWithMetadataLength - len(buf) metaLength := bufWithMetadataLength - len(buf)

View File

@@ -383,18 +383,74 @@ func TestColorPreview(t *testing.T) {
} }
} }
func TestTaskList(t *testing.T) { func TestMarkdownFrontmatter(t *testing.T) {
testcases := []struct { testcases := []struct {
testcase string name string
input string
expected 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. // data-source-position should take into account YAML frontmatter.
"ListAfterFrontmatter",
`--- `---
foo: bar foo: bar
--- ---
- [ ] task 1`, - [ ] 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> <thead>
<tr> <tr>
<th>foo</th> <th>foo</th>
@@ -414,9 +470,9 @@ foo: bar
} }
for _, test := range testcases { for _, test := range testcases {
res, err := markdown.RenderString(markup.NewTestRenderContext(), test.testcase) res, err := markdown.RenderString(markup.NewTestRenderContext(), test.input)
assert.NoError(t, err, "Unexpected error in testcase: %q", test.testcase) assert.NoError(t, err, "Unexpected error in testcase: %q", test.name)
assert.Equal(t, template.HTML(test.expected), res, "Unexpected result in testcase %q", test.testcase) assert.Equal(t, test.expected, string(res), "Unexpected result in testcase %q", test.name)
} }
} }

View File

@@ -16,7 +16,6 @@ import (
// RenderConfig represents rendering configuration for this file // RenderConfig represents rendering configuration for this file
type RenderConfig struct { type RenderConfig struct {
Meta markup.RenderMetaMode Meta markup.RenderMetaMode
Icon string
TOC string // "false": hide, "side"/empty: in sidebar, "main"/"true": in main view TOC string // "false": hide, "side"/empty: in sidebar, "main"/"true": in main view
Lang string Lang string
yamlNode *yaml.Node yamlNode *yaml.Node
@@ -74,7 +73,7 @@ func (rc *RenderConfig) UnmarshalYAML(value *yaml.Node) error {
type yamlRenderConfig struct { type yamlRenderConfig struct {
Meta *string `yaml:"meta"` 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"` TOC *string `yaml:"include_toc"`
Lang *string `yaml:"lang"` Lang *string `yaml:"lang"`
} }
@@ -96,10 +95,6 @@ func (rc *RenderConfig) UnmarshalYAML(value *yaml.Node) error {
rc.Meta = renderMetaModeFromString(*cfg.Gitea.Meta) 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 != "" { if cfg.Gitea.Lang != nil && *cfg.Gitea.Lang != "" {
rc.Lang = *cfg.Gitea.Lang rc.Lang = *cfg.Gitea.Lang
} }
@@ -111,7 +106,7 @@ func (rc *RenderConfig) UnmarshalYAML(value *yaml.Node) error {
return nil return nil
} }
func (rc *RenderConfig) toMetaNode() ast.Node { func (rc *RenderConfig) toMetaNode(g *ASTTransformer) ast.Node {
if rc.yamlNode == nil { if rc.yamlNode == nil {
return nil return nil
} }
@@ -119,7 +114,7 @@ func (rc *RenderConfig) toMetaNode() ast.Node {
case markup.RenderMetaAsTable: case markup.RenderMetaAsTable:
return nodeToTable(rc.yamlNode) return nodeToTable(rc.yamlNode)
case markup.RenderMetaAsDetails: case markup.RenderMetaAsDetails:
return nodeToDetails(rc.yamlNode, rc.Icon) return nodeToDetails(g, rc.yamlNode)
default: default:
return nil return nil
} }

View File

@@ -7,6 +7,7 @@ import (
"strings" "strings"
"testing" "testing"
"github.com/stretchr/testify/assert"
"gopkg.in/yaml.v3" "gopkg.in/yaml.v3"
) )
@@ -19,42 +20,36 @@ func TestRenderConfig_UnmarshalYAML(t *testing.T) {
{ {
"empty", &RenderConfig{ "empty", &RenderConfig{
Meta: "table", Meta: "table",
Icon: "table",
Lang: "", Lang: "",
}, "", }, "",
}, },
{ {
"lang", &RenderConfig{ "lang", &RenderConfig{
Meta: "table", Meta: "table",
Icon: "table",
Lang: "test", Lang: "test",
}, "lang: test", }, "lang: test",
}, },
{ {
"metatable", &RenderConfig{ "metatable", &RenderConfig{
Meta: "table", Meta: "table",
Icon: "table",
Lang: "", Lang: "",
}, "gitea: table", }, "gitea: table",
}, },
{ {
"metanone", &RenderConfig{ "metanone", &RenderConfig{
Meta: "none", Meta: "none",
Icon: "table",
Lang: "", Lang: "",
}, "gitea: none", }, "gitea: none",
}, },
{ {
"metadetails", &RenderConfig{ "metadetails", &RenderConfig{
Meta: "details", Meta: "details",
Icon: "table",
Lang: "", Lang: "",
}, "gitea: details", }, "gitea: details",
}, },
{ {
"metawrong", &RenderConfig{ "metawrong", &RenderConfig{
Meta: "details", Meta: "details",
Icon: "table",
Lang: "", Lang: "",
}, "gitea: wrong", }, "gitea: wrong",
}, },
@@ -62,7 +57,6 @@ func TestRenderConfig_UnmarshalYAML(t *testing.T) {
"toc", &RenderConfig{ "toc", &RenderConfig{
TOC: "true", TOC: "true",
Meta: "table", Meta: "table",
Icon: "table",
Lang: "", Lang: "",
}, "include_toc: true", }, "include_toc: true",
}, },
@@ -70,14 +64,12 @@ func TestRenderConfig_UnmarshalYAML(t *testing.T) {
"tocfalse", &RenderConfig{ "tocfalse", &RenderConfig{
TOC: "false", TOC: "false",
Meta: "table", Meta: "table",
Icon: "table",
Lang: "", Lang: "",
}, "include_toc: false", }, "include_toc: false",
}, },
{ {
"toclang", &RenderConfig{ "toclang", &RenderConfig{
Meta: "table", Meta: "table",
Icon: "table",
TOC: "true", TOC: "true",
Lang: "testlang", Lang: "testlang",
}, ` }, `
@@ -88,7 +80,6 @@ func TestRenderConfig_UnmarshalYAML(t *testing.T) {
{ {
"complexlang", &RenderConfig{ "complexlang", &RenderConfig{
Meta: "table", Meta: "table",
Icon: "table",
Lang: "testlang", Lang: "testlang",
}, ` }, `
gitea: gitea:
@@ -98,7 +89,6 @@ func TestRenderConfig_UnmarshalYAML(t *testing.T) {
{ {
"complexlang2", &RenderConfig{ "complexlang2", &RenderConfig{
Meta: "table", Meta: "table",
Icon: "table",
Lang: "testlang", Lang: "testlang",
}, ` }, `
lang: notright lang: notright
@@ -109,7 +99,6 @@ func TestRenderConfig_UnmarshalYAML(t *testing.T) {
{ {
"complexlang", &RenderConfig{ "complexlang", &RenderConfig{
Meta: "table", Meta: "table",
Icon: "table",
Lang: "testlang", Lang: "testlang",
}, ` }, `
gitea: gitea:
@@ -121,7 +110,6 @@ func TestRenderConfig_UnmarshalYAML(t *testing.T) {
Lang: "two", Lang: "two",
Meta: "table", Meta: "table",
TOC: "true", TOC: "true",
Icon: "smiley",
}, ` }, `
lang: one lang: one
include_toc: true include_toc: true
@@ -137,7 +125,6 @@ func TestRenderConfig_UnmarshalYAML(t *testing.T) {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
got := &RenderConfig{ got := &RenderConfig{
Meta: "table", Meta: "table",
Icon: "table",
Lang: "", Lang: "",
} }
if err := yaml.Unmarshal([]byte(strings.ReplaceAll(tt.args, "\t", " ")), got); err != nil { if err := yaml.Unmarshal([]byte(strings.ReplaceAll(tt.args, "\t", " ")), got); err != nil {
@@ -145,18 +132,9 @@ func TestRenderConfig_UnmarshalYAML(t *testing.T) {
return return
} }
if got.Meta != tt.expected.Meta { assert.Equal(t, tt.expected.Meta, got.Meta)
t.Errorf("Meta Expected %s Got %s", tt.expected.Meta, got.Meta) assert.Equal(t, tt.expected.Lang, got.Lang)
} assert.Equal(t, tt.expected.TOC, got.TOC)
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)
}
}) })
} }
} }

View File

@@ -26,6 +26,7 @@ type ConfigKey interface {
In(defaultVal string, candidates []string) string In(defaultVal string, candidates []string) string
String() string String() string
Strings(delim string) []string Strings(delim string) []string
Bool() (bool, error)
MustString(defaultVal string) string MustString(defaultVal string) string
MustBool(defaultVal ...bool) bool MustBool(defaultVal ...bool) bool

View File

@@ -43,7 +43,8 @@ var Service = struct {
ShowRegistrationButton bool ShowRegistrationButton bool
EnablePasswordSignInForm bool EnablePasswordSignInForm bool
ShowMilestonesDashboardPage bool ShowMilestonesDashboardPage bool
RequireSignInView bool RequireSignInViewStrict bool
BlockAnonymousAccessExpensive bool
EnableNotifyMail bool EnableNotifyMail bool
EnableBasicAuth bool EnableBasicAuth bool
EnablePasskeyAuth bool EnablePasskeyAuth bool
@@ -159,7 +160,18 @@ func loadServiceFrom(rootCfg ConfigProvider) {
Service.EmailDomainBlockList = CompileEmailGlobList(sec, "EMAIL_DOMAIN_BLOCKLIST") Service.EmailDomainBlockList = CompileEmailGlobList(sec, "EMAIL_DOMAIN_BLOCKLIST")
Service.ShowRegistrationButton = sec.Key("SHOW_REGISTRATION_BUTTON").MustBool(!(Service.DisableRegistration || Service.AllowOnlyExternalRegistration)) Service.ShowRegistrationButton = sec.Key("SHOW_REGISTRATION_BUTTON").MustBool(!(Service.DisableRegistration || Service.AllowOnlyExternalRegistration))
Service.ShowMilestonesDashboardPage = sec.Key("SHOW_MILESTONES_DASHBOARD_PAGE").MustBool(true) 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.EnableBasicAuth = sec.Key("ENABLE_BASIC_AUTHENTICATION").MustBool(true)
Service.EnablePasswordSignInForm = sec.Key("ENABLE_PASSWORD_SIGNIN_FORM").MustBool(true) Service.EnablePasswordSignInForm = sec.Key("ENABLE_PASSWORD_SIGNIN_FORM").MustBool(true)
Service.EnablePasskeyAuth = sec.Key("ENABLE_PASSKEY_AUTHENTICATION").MustBool(true) Service.EnablePasskeyAuth = sec.Key("ENABLE_PASSKEY_AUTHENTICATION").MustBool(true)

View File

@@ -7,16 +7,14 @@ import (
"testing" "testing"
"code.gitea.io/gitea/modules/structs" "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/test"
"github.com/gobwas/glob" "github.com/gobwas/glob"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
) )
func TestLoadServices(t *testing.T) { func TestLoadServices(t *testing.T) {
oldService := Service defer test.MockVariableValue(&Service)()
defer func() {
Service = oldService
}()
cfg, err := NewConfigProviderFromData(` cfg, err := NewConfigProviderFromData(`
[service] [service]
@@ -48,10 +46,7 @@ EMAIL_DOMAIN_BLOCKLIST = d3, *.b
} }
func TestLoadServiceVisibilityModes(t *testing.T) { func TestLoadServiceVisibilityModes(t *testing.T) {
oldService := Service defer test.MockVariableValue(&Service)()
defer func() {
Service = oldService
}()
kases := map[string]func(){ 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)
}

View File

@@ -2699,6 +2699,7 @@ branch.restore_success = Branch "%s" has been restored.
branch.restore_failed = Failed to restore branch "%s". branch.restore_failed = Failed to restore branch "%s".
branch.protected_deletion_failed = Branch "%s" is protected. It cannot be deleted. 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_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.restore = Restore Branch "%s"
branch.download = Download Branch "%s" branch.download = Download Branch "%s"
branch.rename = Rename Branch "%s" branch.rename = Rename Branch "%s"

View File

@@ -51,7 +51,7 @@ func apiError(ctx *context.Context, status int, obj any) {
// https://rust-lang.github.io/rfcs/2789-sparse-index.html // https://rust-lang.github.io/rfcs/2789-sparse-index.html
func RepositoryConfig(ctx *context.Context) { 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) { func EnumeratePackageVersions(ctx *context.Context) {

View File

@@ -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) // ReqContainerAccess is a middleware which checks the current user valid (real user or ghost if anonymous access is enabled)
func ReqContainerAccess(ctx *context.Context) { 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) apiUnauthorizedError(ctx)
} }
} }
@@ -152,7 +152,7 @@ func Authenticate(ctx *context.Context) {
u := ctx.Doer u := ctx.Doer
packageScope := auth_service.GetAccessScope(ctx.Data) packageScope := auth_service.GetAccessScope(ctx.Data)
if u == nil { if u == nil {
if setting.Service.RequireSignInView { if setting.Service.RequireSignInViewStrict {
apiUnauthorizedError(ctx) apiUnauthorizedError(ctx)
return return
} }

View File

@@ -356,7 +356,7 @@ func reqToken() func(ctx *context.APIContext) {
func reqExploreSignIn() func(ctx *context.APIContext) { func reqExploreSignIn() func(ctx *context.APIContext) {
return 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") 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(apiAuth(buildAuthGroup()))
m.Use(verifyAuthWithOptions(&common.VerifyOptions{ m.Use(verifyAuthWithOptions(&common.VerifyOptions{
SignInRequired: setting.Service.RequireSignInView, SignInRequired: setting.Service.RequireSignInViewStrict,
})) }))
addActionsRoutes := func( addActionsRoutes := func(

View 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
}

View 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"))
}

View File

@@ -156,7 +156,7 @@ func Install(ctx *context.Context) {
form.DisableRegistration = setting.Service.DisableRegistration form.DisableRegistration = setting.Service.DisableRegistration
form.AllowOnlyExternalRegistration = setting.Service.AllowOnlyExternalRegistration form.AllowOnlyExternalRegistration = setting.Service.AllowOnlyExternalRegistration
form.EnableCaptcha = setting.Service.EnableCaptcha form.EnableCaptcha = setting.Service.EnableCaptcha
form.RequireSignInView = setting.Service.RequireSignInView form.RequireSignInView = setting.Service.RequireSignInViewStrict
form.DefaultKeepEmailPrivate = setting.Service.DefaultKeepEmailPrivate form.DefaultKeepEmailPrivate = setting.Service.DefaultKeepEmailPrivate
form.DefaultAllowCreateOrganization = setting.Service.DefaultAllowCreateOrganization form.DefaultAllowCreateOrganization = setting.Service.DefaultAllowCreateOrganization
form.DefaultEnableTimetracking = setting.Service.DefaultEnableTimetracking form.DefaultEnableTimetracking = setting.Service.DefaultEnableTimetracking

View File

@@ -286,7 +286,7 @@ func ServCommand(ctx *context.PrivateContext) {
repo.IsPrivate || repo.IsPrivate ||
owner.Visibility.IsPrivate() || owner.Visibility.IsPrivate() ||
(user != nil && user.IsRestricted) || // user will be nil if the key is a deploykey (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 key.Type == asymkey_model.KeyTypeDeploy {
if deployKey.Mode < mode { if deployKey.Mode < mode {
ctx.JSON(http.StatusUnauthorized, private.Response{ ctx.JSON(http.StatusUnauthorized, private.Response{

View File

@@ -4,26 +4,12 @@
package web package web
import ( import (
"net/http"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/web" "code.gitea.io/gitea/modules/web"
"code.gitea.io/gitea/routers/web/repo" "code.gitea.io/gitea/routers/web/repo"
"code.gitea.io/gitea/services/context" "code.gitea.io/gitea/services/context"
) )
func addOwnerRepoGitHTTPRouters(m *web.Router) { 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.Group("/{username}/{reponame}", func() {
m.Methods("POST,OPTIONS", "/git-upload-pack", repo.ServiceUploadPack) m.Methods("POST,OPTIONS", "/git-upload-pack", repo.ServiceUploadPack)
m.Methods("POST,OPTIONS", "/git-receive-pack", repo.ServiceReceivePack) 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/{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}}.pack", repo.GetPackFile)
m.Methods("GET,OPTIONS", "/objects/pack/pack-{file:[0-9a-f]{40,64}}.idx", repo.GetIdxFile) 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())
} }

View File

@@ -6,6 +6,7 @@ package actions
import ( import (
"bytes" "bytes"
stdCtx "context" stdCtx "context"
"errors"
"fmt" "fmt"
"net/http" "net/http"
"slices" "slices"
@@ -77,7 +78,11 @@ func List(ctx *context.Context) {
return return
} else if !empty { } else if !empty {
commit, err := ctx.Repo.GitRepo.GetBranchCommit(ctx.Repo.Repository.DefaultBranch) 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) ctx.ServerError("GetBranchCommit", err)
return return
} }

View File

@@ -8,6 +8,7 @@ import (
"time" "time"
activities_model "code.gitea.io/gitea/models/activities" activities_model "code.gitea.io/gitea/models/activities"
"code.gitea.io/gitea/models/git"
"code.gitea.io/gitea/models/unit" "code.gitea.io/gitea/models/unit"
"code.gitea.io/gitea/modules/base" "code.gitea.io/gitea/modules/base"
"code.gitea.io/gitea/services/context" "code.gitea.io/gitea/services/context"
@@ -52,12 +53,26 @@ func Activity(ctx *context.Context) {
ctx.Data["DateUntil"] = timeUntil ctx.Data["DateUntil"] = timeUntil
ctx.Data["PeriodText"] = ctx.Tr("repo.activity.period." + ctx.Data["Period"].(string)) 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 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.TypeReleases),
ctx.Repo.CanRead(unit.TypeIssues), ctx.Repo.CanRead(unit.TypeIssues),
ctx.Repo.CanRead(unit.TypePullRequests), ctx.Repo.CanRead(unit.TypePullRequests),
ctx.Repo.CanRead(unit.TypeCode)); err != nil { canReadCode,
)
if err != nil {
ctx.ServerError("GetActivityStats", err) ctx.ServerError("GetActivityStats", err)
return return
} }

View File

@@ -34,7 +34,7 @@ func CodeFrequencyData(ctx *context.Context) {
ctx.Status(http.StatusAccepted) ctx.Status(http.StatusAccepted)
return return
} }
ctx.ServerError("GetCodeFrequencyData", err) ctx.ServerError("GetContributorStats", err)
} else { } else {
ctx.JSON(http.StatusOK, contributorStats["total"].Weeks) ctx.JSON(http.StatusOK, contributorStats["total"].Weeks)
} }

View File

@@ -127,7 +127,7 @@ func httpBase(ctx *context.Context) *serviceHandler {
// Only public pull don't need auth. // Only public pull don't need auth.
isPublicPull := repoExist && !repo.IsPrivate && isPull isPublicPull := repoExist && !repo.IsPrivate && isPull
var ( var (
askAuth = !isPublicPull || setting.Service.RequireSignInView askAuth = !isPublicPull || setting.Service.RequireSignInViewStrict
environ []string environ []string
) )

View File

@@ -4,12 +4,10 @@
package repo package repo
import ( import (
"errors"
"net/http" "net/http"
"code.gitea.io/gitea/modules/base" "code.gitea.io/gitea/modules/base"
"code.gitea.io/gitea/services/context" "code.gitea.io/gitea/services/context"
contributors_service "code.gitea.io/gitea/services/repository"
) )
const ( const (
@@ -26,16 +24,3 @@ func RecentCommits(ctx *context.Context) {
ctx.HTML(http.StatusOK, tplRecentCommits) 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)
}
}

View File

@@ -9,7 +9,6 @@ import (
"image" "image"
"io" "io"
"path" "path"
"slices"
"strings" "strings"
git_model "code.gitea.io/gitea/models/git" git_model "code.gitea.io/gitea/models/git"
@@ -79,7 +78,7 @@ func prepareToRenderFile(ctx *context.Context, entry *git.TreeEntry) {
if workFlowErr != nil { if workFlowErr != nil {
ctx.Data["FileError"] = ctx.Locale.Tr("actions.runs.invalid_workflow_helper", workFlowErr.Error()) 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 { if data, err := blob.GetBlobContent(setting.UI.MaxDisplayFileSize); err == nil {
_, warnings := issue_model.GetCodeOwnersFromContent(ctx, data) _, warnings := issue_model.GetCodeOwnersFromContent(ctx, data)
if len(warnings) > 0 { if len(warnings) > 0 {

View File

@@ -285,23 +285,23 @@ func Routes() *web.Router {
mid = append(mid, repo.GetActiveStopwatch) mid = append(mid, repo.GetActiveStopwatch)
mid = append(mid, goGet) mid = append(mid, goGet)
others := web.NewRouter() webRoutes := web.NewRouter()
others.Use(mid...) webRoutes.Use(mid...)
registerRoutes(others) webRoutes.Group("", func() { registerWebRoutes(webRoutes) }, common.BlockExpensive())
routes.Mount("", others) routes.Mount("", webRoutes)
return routes return routes
} }
var optSignInIgnoreCsrf = verifyAuthWithOptions(&common.VerifyOptions{DisableCSRF: true}) var optSignInIgnoreCsrf = verifyAuthWithOptions(&common.VerifyOptions{DisableCSRF: true})
// registerRoutes register routes // registerWebRoutes register routes
func registerRoutes(m *web.Router) { func registerWebRoutes(m *web.Router) {
// required to be signed in or signed out // required to be signed in or signed out
reqSignIn := verifyAuthWithOptions(&common.VerifyOptions{SignInRequired: true}) reqSignIn := verifyAuthWithOptions(&common.VerifyOptions{SignInRequired: true})
reqSignOut := verifyAuthWithOptions(&common.VerifyOptions{SignOutRequired: true}) reqSignOut := verifyAuthWithOptions(&common.VerifyOptions{SignOutRequired: true})
// optional sign in (if signed in, use the user as doer, if not, no doer) // optional sign in (if signed in, use the user as doer, if not, no doer)
optSignIn := verifyAuthWithOptions(&common.VerifyOptions{SignInRequired: setting.Service.RequireSignInView}) optSignIn := verifyAuthWithOptions(&common.VerifyOptions{SignInRequired: setting.Service.RequireSignInViewStrict})
optExploreSignIn := verifyAuthWithOptions(&common.VerifyOptions{SignInRequired: setting.Service.RequireSignInView || setting.Service.Explore.RequireSigninView}) optExploreSignIn := verifyAuthWithOptions(&common.VerifyOptions{SignInRequired: setting.Service.RequireSignInViewStrict || setting.Service.Explore.RequireSigninView})
validation.AddBindingRules() validation.AddBindingRules()
@@ -1454,20 +1454,23 @@ func registerRoutes(m *web.Router) {
m.Group("/{username}/{reponame}/activity", func() { m.Group("/{username}/{reponame}/activity", func() {
m.Get("", repo.Activity) m.Get("", repo.Activity)
m.Get("/{period}", repo.Activity) m.Get("/{period}", repo.Activity)
m.Group("/contributors", func() {
m.Get("", repo.Contributors) m.Group("", func() {
m.Get("/data", repo.ContributorsData) m.Group("/contributors", func() {
}) m.Get("", repo.Contributors)
m.Group("/code-frequency", func() { m.Get("/data", repo.ContributorsData)
m.Get("", repo.CodeFrequency) })
m.Get("/data", repo.CodeFrequencyData) m.Group("/code-frequency", func() {
}) m.Get("", repo.CodeFrequency)
m.Group("/recent-commits", func() { m.Get("/data", repo.CodeFrequencyData)
m.Get("", repo.RecentCommits) })
m.Get("/data", repo.RecentCommitsData) 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, context.RepoRef(), repo.MustBeNotEmpty,
) )
// end "/{username}/{reponame}/activity" // end "/{username}/{reponame}/activity"

View File

@@ -151,7 +151,7 @@ func (ctx *Context) notFoundInternal(logMsg string, logErr error) {
ctx.Data["IsRepo"] = ctx.Repo.Repository != nil ctx.Data["IsRepo"] = ctx.Repo.Repository != nil
ctx.Data["Title"] = "Page Not Found" 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. // ServerError displays a 500 (Internal Server Error) page and prints the given error, if any.

View File

@@ -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) { 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 return perm.AccessModeNone, nil
} }

View File

@@ -356,7 +356,9 @@ func RedirectToRepo(ctx *Base, redirectRepoID int64) {
if ctx.Req.URL.RawQuery != "" { if ctx.Req.URL.RawQuery != "" {
redirectPath += "?" + 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) { func repoAssignment(ctx *Context, repo *repo_model.Repository) {

View File

@@ -121,7 +121,7 @@ func checkStorage(opts *checkStorageOptions) func(ctx context.Context, logger lo
storer: storage.LFS, storer: storage.LFS,
isOrphaned: func(path string, obj storage.Object, stat fs.FileInfo) (bool, error) { 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 // 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) exists, err := git.ExistsLFSObject(ctx, oid)
return !exists, err return !exists, err
}, },

View File

@@ -92,8 +92,12 @@ func ChangeTitle(ctx context.Context, issue *issues_model.Issue, doer *user_mode
var reviewNotifiers []*ReviewRequestNotifier var reviewNotifiers []*ReviewRequestNotifier
if issue.IsPull && issues_model.HasWorkInProgressPrefix(oldTitle) && !issues_model.HasWorkInProgressPrefix(title) { if issue.IsPull && issues_model.HasWorkInProgressPrefix(oldTitle) && !issues_model.HasWorkInProgressPrefix(title) {
if err := issue.LoadPullRequest(ctx); err != nil {
return err
}
var err error var err error
reviewNotifiers, err = PullRequestCodeOwnersReview(ctx, issue, issue.PullRequest) reviewNotifiers, err = PullRequestCodeOwnersReview(ctx, issue.PullRequest)
if err != nil { if err != nil {
log.Error("PullRequestCodeOwnersReview: %v", err) log.Error("PullRequestCodeOwnersReview: %v", err)
} }

View File

@@ -6,6 +6,7 @@ package issue
import ( import (
"context" "context"
"fmt" "fmt"
"slices"
"time" "time"
issues_model "code.gitea.io/gitea/models/issues" issues_model "code.gitea.io/gitea/models/issues"
@@ -40,20 +41,31 @@ type ReviewRequestNotifier struct {
ReviewTeam *org_model.Team ReviewTeam *org_model.Team
} }
func PullRequestCodeOwnersReview(ctx context.Context, issue *issues_model.Issue, pr *issues_model.PullRequest) ([]*ReviewRequestNotifier, error) { var codeOwnerFiles = []string{"CODEOWNERS", "docs/CODEOWNERS", ".gitea/CODEOWNERS"}
files := []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) { if pr.IsWorkInProgress(ctx) {
return nil, nil return nil, nil
} }
if err := pr.LoadHeadRepo(ctx); err != nil { if err := pr.LoadHeadRepo(ctx); err != nil {
return nil, err return nil, err
} }
if err := pr.LoadBaseRepo(ctx); err != nil { if err := pr.LoadBaseRepo(ctx); err != nil {
return nil, err return nil, err
} }
pr.Issue.Repo = pr.BaseRepo
if pr.BaseRepo.IsFork { if pr.BaseRepo.IsFork {
return nil, nil return nil, nil
@@ -71,7 +83,7 @@ func PullRequestCodeOwnersReview(ctx context.Context, issue *issues_model.Issue,
} }
var data string var data string
for _, file := range files { for _, file := range codeOwnerFiles {
if blob, err := commit.GetBlobByPath(file); err == nil { if blob, err := commit.GetBlobByPath(file); err == nil {
data, err = blob.GetBlobContent(setting.UI.MaxDisplayFileSize) data, err = blob.GetBlobContent(setting.UI.MaxDisplayFileSize)
if err == nil { 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) rules, _ := issues_model.GetCodeOwnersFromContent(ctx, data)
if len(rules) == 0 {
return nil, nil
}
// get the mergebase if startCommitID == "" && endCommitID == "" {
mergeBase, err := getMergeBase(repo, pr, git.BranchPrefix+pr.BaseBranch, pr.GetGitRefName()) // get the mergebase
if err != nil { mergeBase, err := getMergeBase(repo, pr, git.BranchPrefix+pr.BaseBranch, pr.GetGitRefName())
return nil, err 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 // 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 // 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 { if err != nil {
return nil, err return nil, err
} }

View File

@@ -248,7 +248,7 @@ func createOrUpdateConfigFile(ctx context.Context, repo *repo_model.Repository,
"Initialize Cargo Config", "Initialize Cargo Config",
func(t *files_service.TemporaryUploadRepository) error { func(t *files_service.TemporaryUploadRepository) error {
var b bytes.Buffer 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 { if err != nil {
return err return err
} }

View File

@@ -408,7 +408,6 @@ func buildPrimary(ctx context.Context, pv *packages_model.PackageVersion, pfs []
files = append(files, f) files = append(files, f)
} }
} }
packageVersion := fmt.Sprintf("%s-%s", pd.FileMetadata.Version, pd.FileMetadata.Release)
packages = append(packages, &Package{ packages = append(packages, &Package{
Type: "rpm", Type: "rpm",
Name: pd.Package.Name, Name: pd.Package.Name,
@@ -437,7 +436,7 @@ func buildPrimary(ctx context.Context, pv *packages_model.PackageVersion, pfs []
Archive: pd.FileMetadata.ArchiveSize, Archive: pd.FileMetadata.ArchiveSize,
}, },
Location: Location{ 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{ Format: Format{
License: pd.VersionMetadata.License, License: pd.VersionMetadata.License,

View File

@@ -189,7 +189,15 @@ func Merge(ctx context.Context, pr *issues_model.PullRequest, doer *user_model.U
} }
defer releaser() defer releaser()
defer func() { 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) _, err = doMergeAndPush(ctx, pr, doer, mergeStyle, expectedHeadCommitID, message, repo_module.PushTriggerPRMergeToBase)

View File

@@ -176,7 +176,7 @@ func NewPullRequest(ctx context.Context, opts *NewPullRequestOptions) error {
} }
if !pr.IsWorkInProgress(ctx) { if !pr.IsWorkInProgress(ctx) {
reviewNotifiers, err = issue_service.PullRequestCodeOwnersReview(ctx, issue, pr) reviewNotifiers, err = issue_service.PullRequestCodeOwnersReview(ctx, pr)
if err != nil { if err != nil {
return err return err
} }
@@ -349,19 +349,29 @@ func checkForInvalidation(ctx context.Context, requests issues_model.PullRequest
return nil 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, // AddTestPullRequestTask adds new test tasks by given head/base repository and head/base branch,
// and generate new patch for testing as needed. // and generate new patch for testing as needed.
func AddTestPullRequestTask(doer *user_model.User, repoID int64, branch string, isSync bool, oldCommitID, newCommitID string) { func AddTestPullRequestTask(opts TestPullRequestOptions) {
log.Trace("AddTestPullRequestTask [head_repo_id: %d, head_branch: %s]: finding pull requests", repoID, branch) log.Trace("AddTestPullRequestTask [head_repo_id: %d, head_branch: %s]: finding pull requests", opts.RepoID, opts.Branch)
graceful.GetManager().RunWithShutdownContext(func(ctx context.Context) { graceful.GetManager().RunWithShutdownContext(func(ctx context.Context) {
// There is no sensible way to shut this down ":-(" // There is no sensible way to shut this down ":-("
// If you don't let it run all the way then you will lose data // If you don't let it run all the way then you will lose data
// TODO: graceful: AddTestPullRequestTask needs to become a queue! // TODO: graceful: AddTestPullRequestTask needs to become a queue!
// GetUnmergedPullRequestsByHeadInfo() only return open and unmerged PR. // 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 { 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 return
} }
@@ -377,25 +387,24 @@ func AddTestPullRequestTask(doer *user_model.User, repoID int64, branch string,
} }
AddToTaskQueue(ctx, pr) 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 { if err == nil && comment != nil {
notify_service.PullRequestPushCommits(ctx, doer, pr, comment) notify_service.PullRequestPushCommits(ctx, opts.Doer, pr, comment)
} }
} }
if isSync { if opts.IsSync {
requests := issues_model.PullRequestList(prs) if err = issues_model.PullRequestList(prs).LoadAttributes(ctx); err != nil {
if err = requests.LoadAttributes(ctx); err != nil {
log.Error("PullRequestList.LoadAttributes: %v", err) 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) log.Error("checkForInvalidation: %v", invalidationErr)
} }
if err == nil { if err == nil {
for _, pr := range prs { for _, pr := range prs {
objectFormat := git.ObjectFormatFromName(pr.BaseRepo.ObjectFormatName) objectFormat := git.ObjectFormatFromName(pr.BaseRepo.ObjectFormatName)
if newCommitID != "" && newCommitID != objectFormat.EmptyObjectID().String() { if opts.NewCommitID != "" && opts.NewCommitID != objectFormat.EmptyObjectID().String() {
changed, err := checkIfPRContentChanged(ctx, pr, oldCommitID, newCommitID) changed, err := checkIfPRContentChanged(ctx, pr, opts.OldCommitID, opts.NewCommitID)
if err != nil { if err != nil {
log.Error("checkIfPRContentChanged: %v", err) log.Error("checkIfPRContentChanged: %v", err)
} }
@@ -411,12 +420,12 @@ func AddTestPullRequestTask(doer *user_model.User, repoID int64, branch string,
log.Error("GetFirstMatchProtectedBranchRule: %v", err) log.Error("GetFirstMatchProtectedBranchRule: %v", err)
} }
if pb != nil && pb.DismissStaleApprovals { 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) 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) log.Error("MarkReviewsAsNotStale: %v", err)
} }
divergence, err := GetDiverging(ctx, pr) 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) log.Trace("AddTestPullRequestTask [base_repo_id: %d, base_branch: %s]: finding pull requests", opts.RepoID, opts.Branch)
prs, err = issues_model.GetUnmergedPullRequestsByBaseInfo(ctx, repoID, branch) prs, err = issues_model.GetUnmergedPullRequestsByBaseInfo(ctx, opts.RepoID, opts.Branch)
if err != nil { 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 return
} }
for _, pr := range prs { for _, pr := range prs {

View File

@@ -42,7 +42,15 @@ func Update(ctx context.Context, pr *issues_model.PullRequest, doer *user_model.
if rebase { if rebase {
defer func() { 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) 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) _, err = doMergeAndPush(ctx, reversePR, doer, repo_model.MergeStyleMerge, "", message, repository.PushTriggerPRUpdateWithBase)
defer func() { 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 return err

View File

@@ -170,7 +170,6 @@ func pushUpdates(optsList []*repo_module.PushUpdateOptions) error {
branch := opts.RefFullName.BranchName() branch := opts.RefFullName.BranchName()
if !opts.IsDelRef() { if !opts.IsDelRef() {
log.Trace("TriggerTask '%s/%s' by %s", repo.Name, branch, pusher.Name) 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) newCommit, err := gitRepo.GetCommit(opts.NewCommitID)
if err != nil { 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) 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 { if isForcePush {
log.Trace("Push %s is a force push", opts.NewCommitID) log.Trace("Push %s is a force push", opts.NewCommitID)

View File

@@ -14,6 +14,7 @@ import (
"unicode/utf8" "unicode/utf8"
webhook_model "code.gitea.io/gitea/models/webhook" 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/git"
"code.gitea.io/gitea/modules/json" "code.gitea.io/gitea/modules/json"
"code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/log"
@@ -101,6 +102,13 @@ var (
redColor = color("ff3232") 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 { type discordConvertor struct {
Username string Username string
AvatarURL string AvatarURL string
@@ -307,7 +315,7 @@ func (d discordConvertor) createPayload(s *api.User, title, text, url string, co
Embeds: []DiscordEmbed{ Embeds: []DiscordEmbed{
{ {
Title: title, Title: title,
Description: text, Description: base.TruncateString(text, discordDescriptionCharactersLimit),
URL: url, URL: url,
Color: color, Color: color,
Author: DiscordEmbedAuthor{ Author: DiscordEmbedAuthor{

View File

@@ -148,7 +148,7 @@
<dt>{{ctx.Locale.Tr "admin.config.enable_openid_signin"}}</dt> <dt>{{ctx.Locale.Tr "admin.config.enable_openid_signin"}}</dt>
<dd>{{svg (Iif .Service.EnableOpenIDSignIn "octicon-check" "octicon-x")}}</dd> <dd>{{svg (Iif .Service.EnableOpenIDSignIn "octicon-check" "octicon-x")}}</dd>
<dt>{{ctx.Locale.Tr "admin.config.require_sign_in_view"}}</dt> <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> <dt>{{ctx.Locale.Tr "admin.config.mail_notify"}}</dt>
<dd>{{svg (Iif .Service.EnableNotifyMail "octicon-check" "octicon-x")}}</dd> <dd>{{svg (Iif .Service.EnableNotifyMail "octicon-check" "octicon-x")}}</dd>
<dt>{{ctx.Locale.Tr "admin.config.enable_captcha"}}</dt> <dt>{{ctx.Locale.Tr "admin.config.enable_captcha"}}</dt>

View File

@@ -1,10 +1,10 @@
{{$canWriteProject := and .CanWriteProjects (or (not .Repository) (not .Repository.IsArchived))}} {{$canWriteProject := and .CanWriteProjects (or (not .Repository) (not .Repository.IsArchived))}}
<div class="ui container tw-max-w-full"> <div class="ui container tw-max-w-full">
<div class="tw-flex tw-justify-between tw-items-center tw-mb-4 tw-gap-3"> <div class="flex-text-block tw-flex-wrap tw-mb-4">
<h2 class="tw-mb-0 tw-flex-1 tw-break-anywhere">{{.Project.Title}}</h2> <h2 class="tw-mb-0">{{.Project.Title}}</h2>
<div class="project-toolbar-right"> <div class="tw-flex-1"></div>
<div class="ui secondary filter menu labels"> <div class="ui secondary menu tw-m-0">
{{$queryLink := QueryBuild "?" "labels" .SelectLabels "assignee" $.AssigneeID "archived_labels" (Iif $.ShowArchivedLabels "true")}} {{$queryLink := QueryBuild "?" "labels" .SelectLabels "assignee" $.AssigneeID "archived_labels" (Iif $.ShowArchivedLabels "true")}}
{{template "repo/issue/filter_item_label" dict "Labels" .Labels "QueryLink" $queryLink "SupportArchivedLabel" 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") "TextNegativeOne" (ctx.Locale.Tr "repo.issues.filter_assginee_no_assignee")
}} }}
</div> </div>
</div>
{{if $canWriteProject}} {{if $canWriteProject}}
<div class="ui compact mini menu"> <div class="ui compact mini menu">
<a class="item" href="{{.Link}}/edit?redirect=project"> <a class="item" href="{{.Link}}/edit?redirect=project">

View File

@@ -10,12 +10,7 @@
<div class="ui attached segment"> <div class="ui attached segment">
{{template "base/alert" .}} {{template "base/alert" .}}
{{template "repo/create_helper" .}} {{template "repo/create_helper" .}}
<div id="create-repo-error-message" class="ui negative message tw-text-center tw-hidden"></div>
{{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 class="inline required field {{if .Err_Owner}}error{{end}}"> <div class="inline required field {{if .Err_Owner}}error{{end}}">
<label>{{ctx.Locale.Tr "repo.owner"}}</label> <label>{{ctx.Locale.Tr "repo.owner"}}</label>
<div class="ui selection owner dropdown"> <div class="ui selection owner dropdown">
@@ -26,7 +21,11 @@
</span> </span>
{{svg "octicon-triangle-down" 14 "dropdown icon"}} {{svg "octicon-triangle-down" 14 "dropdown icon"}}
<div class="menu"> <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"}} {{ctx.AvatarUtils.Avatar .SignedUser 28 "mini"}}
<span class="truncated-item-name">{{.SignedUser.ShortName 40}}</span> <span class="truncated-item-name">{{.SignedUser.ShortName 40}}</span>
</div> </div>
@@ -209,7 +208,7 @@
<br> <br>
<div class="inline field"> <div class="inline field">
<label></label> <label></label>
<button class="ui primary button{{if not .CanCreateRepo}} disabled{{end}}"> <button class="ui primary button">
{{ctx.Locale.Tr "repo.create_repo"}} {{ctx.Locale.Tr "repo.create_repo"}}
</button> </button>
</div> </div>

View File

@@ -14,9 +14,10 @@
</div> </div>
{{end}} {{end}}
<div class="list-header"> <div class="list-header flex-text-block">
{{template "repo/issue/navbar" .}}
{{template "repo/issue/search" .}} {{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 not .Repository.IsArchived}}
{{if .PageIsIssueList}} {{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> <a class="ui small primary button issue-list-new" href="{{.RepoLink}}/issues/new{{if .NewIssueChooseTemplate}}/choose{{end}}">{{ctx.Locale.Tr "repo.issues.new"}}</a>

View File

@@ -1,14 +1,18 @@
{{$canReadCode := $.Permission.CanRead ctx.Consts.RepoUnitTypeCode}}
<div class="ui fluid vertical menu"> <div class="ui fluid vertical menu">
<a class="{{if .PageIsPulse}}active {{end}}item" href="{{.RepoLink}}/activity"> <a class="{{if .PageIsPulse}}active {{end}}item" href="{{.RepoLink}}/activity">
{{ctx.Locale.Tr "repo.activity.navbar.pulse"}} {{ctx.Locale.Tr "repo.activity.navbar.pulse"}}
</a> </a>
<a class="{{if .PageIsContributors}}active {{end}}item" href="{{.RepoLink}}/activity/contributors"> {{if $canReadCode}}
{{ctx.Locale.Tr "repo.activity.navbar.contributors"}} <a class="{{if .PageIsContributors}}active {{end}}item" href="{{.RepoLink}}/activity/contributors">
</a> {{ctx.Locale.Tr "repo.activity.navbar.contributors"}}
<a class="{{if .PageIsCodeFrequency}}active{{end}} item" href="{{.RepoLink}}/activity/code-frequency"> </a>
{{ctx.Locale.Tr "repo.activity.navbar.code_frequency"}} <a class="{{if .PageIsCodeFrequency}}active{{end}} item" href="{{.RepoLink}}/activity/code-frequency">
</a> {{ctx.Locale.Tr "repo.activity.navbar.code_frequency"}}
<a class="{{if .PageIsRecentCommits}}active{{end}} item" href="{{.RepoLink}}/activity/recent-commits"> </a>
{{ctx.Locale.Tr "repo.activity.navbar.recent_commits"}} <a class="{{if .PageIsRecentCommits}}active{{end}} item" href="{{.RepoLink}}/activity/recent-commits">
</a> {{ctx.Locale.Tr "repo.activity.navbar.recent_commits"}}
</a>
{{end}}
</div> </div>

View File

@@ -2,8 +2,9 @@
<div role="main" aria-label="{{.Title}}" class="page-content repository projects view-project"> <div role="main" aria-label="{{.Title}}" class="page-content repository projects view-project">
{{template "repo/header" .}} {{template "repo/header" .}}
<div class="ui container padded"> <div class="ui container padded">
<div class="tw-flex tw-justify-between tw-items-center tw-mb-4"> <div class="flex-text-block tw-justify-end tw-mb-4">
{{template "repo/issue/navbar" .}} <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> <a class="ui small primary button" href="{{.RepoLink}}/issues/new/choose?project={{.Project.ID}}">{{ctx.Locale.Tr "repo.issues.new"}}</a>
</div> </div>
</div> </div>

View File

@@ -4,7 +4,9 @@
package integration package integration
import ( import (
"encoding/base64"
"fmt" "fmt"
"net/http"
"net/url" "net/url"
"strings" "strings"
"testing" "testing"
@@ -24,7 +26,9 @@ import (
"code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/gitrepo" "code.gitea.io/gitea/modules/gitrepo"
"code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/setting"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/test" "code.gitea.io/gitea/modules/test"
"code.gitea.io/gitea/modules/util"
pull_service "code.gitea.io/gitea/services/pull" pull_service "code.gitea.io/gitea/services/pull"
release_service "code.gitea.io/gitea/services/release" release_service "code.gitea.io/gitea/services/release"
repo_service "code.gitea.io/gitea/services/repository" repo_service "code.gitea.io/gitea/services/repository"
@@ -451,3 +455,109 @@ func TestCreateDeleteRefEvent(t *testing.T) {
assert.NotNil(t, run) 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)
})
}

View File

@@ -148,9 +148,9 @@ func TestAPIOrgEditBadVisibility(t *testing.T) {
func TestAPIOrgDeny(t *testing.T) { func TestAPIOrgDeny(t *testing.T) {
onGiteaRun(t, func(*testing.T, *url.URL) { onGiteaRun(t, func(*testing.T, *url.URL) {
setting.Service.RequireSignInView = true setting.Service.RequireSignInViewStrict = true
defer func() { defer func() {
setting.Service.RequireSignInView = false setting.Service.RequireSignInViewStrict = false
}() }()
orgName := "user1_org" orgName := "user1_org"

View File

@@ -111,7 +111,7 @@ func TestPackageContainer(t *testing.T) {
AddTokenAuth(anonymousToken) AddTokenAuth(anonymousToken)
MakeRequest(t, req, http.StatusOK) 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)) req = NewRequest(t, "GET", fmt.Sprintf("%sv2", setting.AppURL))
MakeRequest(t, req, http.StatusUnauthorized) MakeRequest(t, req, http.StatusUnauthorized)

View File

@@ -15,6 +15,7 @@ import (
"code.gitea.io/gitea/models/unittest" "code.gitea.io/gitea/models/unittest"
user_model "code.gitea.io/gitea/models/user" user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/test"
"code.gitea.io/gitea/tests" "code.gitea.io/gitea/tests"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
@@ -131,11 +132,7 @@ func TestPackageGeneric(t *testing.T) {
t.Run("RequireSignInView", func(t *testing.T) { t.Run("RequireSignInView", func(t *testing.T) {
defer tests.PrintCurrentTest(t)() defer tests.PrintCurrentTest(t)()
defer test.MockVariableValue(&setting.Service.RequireSignInViewStrict, true)()
setting.Service.RequireSignInView = true
defer func() {
setting.Service.RequireSignInView = false
}()
req = NewRequest(t, "GET", url+"/dummy.bin") req = NewRequest(t, "GET", url+"/dummy.bin")
MakeRequest(t, req, http.StatusUnauthorized) MakeRequest(t, req, http.StatusUnauthorized)

View File

@@ -9,11 +9,17 @@ import (
"net/url" "net/url"
"testing" "testing"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/test"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
) )
func TestGitSmartHTTP(t *testing.T) { 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) { 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")
}

View File

@@ -8,6 +8,7 @@ import (
"net/http/httptest" "net/http/httptest"
"net/url" "net/url"
"path" "path"
"sort"
"strings" "strings"
"testing" "testing"
@@ -67,7 +68,7 @@ func TestPullView_CodeOwner(t *testing.T) {
{ {
Operation: "create", Operation: "create",
TreePath: "CODEOWNERS", 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) { t.Run("First Pull Request", func(t *testing.T) {
// create a new branch to prepare for pull request // 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", NewBranch: "codeowner-basebranch",
Files: []*files_service.ChangeRepoFile{ 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}) unittest.AssertExistsIf(t, true, &issues_model.Review{IssueID: pr.IssueID, Type: issues_model.ReviewTypeRequest, ReviewerID: 5})
assert.NoError(t, pr.LoadIssue(db.DefaultContext)) 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) assert.NoError(t, err)
prUpdated1 := unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{ID: pr.ID}) prUpdated1 := unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{ID: pr.ID})
assert.NoError(t, prUpdated1.LoadIssue(db.DefaultContext)) 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") 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"}) 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) { t.Run("Forked Repo Pull Request", func(t *testing.T) {

View File

@@ -4,12 +4,15 @@
package integration package integration
import ( import (
"context"
"net/http" "net/http"
"net/url" "net/url"
"strings" "strings"
"testing" "testing"
"code.gitea.io/gitea/models/db"
repo_model "code.gitea.io/gitea/models/repo" repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/models/unittest"
"code.gitea.io/gitea/modules/test" "code.gitea.io/gitea/modules/test"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
@@ -61,5 +64,14 @@ func TestRepoActivity(t *testing.T) {
// Should be 3 new issues // Should be 3 new issues
list = htmlDoc.doc.Find("#new-issues").Next().Find("p.desc") list = htmlDoc.doc.Find("#new-issues").Next().Find("p.desc")
assert.Len(t, list.Nodes, 3) 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.`)
}) })
} }

View File

@@ -15,11 +15,13 @@ import (
"code.gitea.io/gitea/modules/test" "code.gitea.io/gitea/modules/test"
"code.gitea.io/gitea/modules/translation" "code.gitea.io/gitea/modules/translation"
"code.gitea.io/gitea/modules/web" "code.gitea.io/gitea/modules/web"
"code.gitea.io/gitea/routers"
"code.gitea.io/gitea/services/context" "code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/tests" "code.gitea.io/gitea/tests"
"github.com/markbates/goth" "github.com/markbates/goth"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
) )
func testLoginFailed(t *testing.T, username, password, message string) { 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) 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"))
})
}

View File

@@ -1225,14 +1225,7 @@ table th[data-sortt-desc] .svg {
box-shadow: 0 0 0 1px var(--color-secondary) inset; box-shadow: 0 0 0 1px var(--color-secondary) inset;
} }
.emoji { /* for "image" emojis like ":git:" ":gitea:" and ":github:" (see CUSTOM_EMOJIS config option) */
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;
}
.emoji img { .emoji img {
border-width: 0 !important; border-width: 0 !important;
margin: 0 !important; margin: 0 !important;
@@ -1391,6 +1384,11 @@ table th[data-sortt-desc] .svg {
min-width: 0; 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 /* 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 */ 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) { .ui.dropdown .menu.flex-items-menu > .item:not(.hidden, .filtered, .tw-hidden) {

View File

@@ -8,18 +8,6 @@
margin: 0 0.5em; 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 { .project-column {
background-color: var(--color-project-column-bg) !important; background-color: var(--color-project-column-bg) !important;
border: 1px solid var(--color-secondary) !important; border: 1px solid var(--color-secondary) !important;

View File

@@ -337,11 +337,6 @@
padding-right: 28px; padding-right: 28px;
} }
.markup .emoji {
max-width: none;
vertical-align: text-top;
}
.markup span.frame { .markup span.frame {
display: block; display: block;
overflow: hidden; overflow: hidden;
@@ -521,6 +516,18 @@
padding-left: 2em; 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 { .file-revisions-btn {
display: block; display: block;
float: left; float: left;

View File

@@ -1,11 +1,13 @@
import $ from 'jquery'; import $ from 'jquery';
import {htmlEscape} from 'escape-goat'; 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; const {appSubUrl} = window.config;
export function initRepoTemplateSearch() { export function initRepoTemplateSearch() {
const $repoTemplate = $('#repo_template'); const $repoTemplate = $('#repo_template');
if (!$repoTemplate.length) return; // make sure the current page is "new repo" page
const checkTemplate = function () { const checkTemplate = function () {
const $templateUnits = $('#template_units'); const $templateUnits = $('#template_units');
const $nonTemplate = $('#non_template'); const $nonTemplate = $('#non_template');
@@ -21,6 +23,20 @@ export function initRepoTemplateSearch() {
checkTemplate(); checkTemplate();
const changeOwner = function () { 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') $('#repo_template_search')
.dropdown({ .dropdown({
apiSettings: { apiSettings: {

View 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);
});

View File

@@ -16,3 +16,19 @@ try {
return intlNumberFormat(locales, options); 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;
}