mirror of
https://codeberg.org/forgejo/forgejo.git
synced 2024-05-08 07:02:41 +00:00
Compare commits
48 commits
e766e13556
...
9035b400a6
Author | SHA1 | Date | |
---|---|---|---|
9035b400a6 | |||
d6c36ec406 | |||
20350846fc | |||
c31ae1a651 | |||
0819ed2053 | |||
51a610d46c | |||
2bc226eb57 | |||
801554f708 | |||
c864448dc9 | |||
127eff49ee | |||
5247fd50db | |||
3dfa5ba43a | |||
90c56a9d66 | |||
aa2af10f67 | |||
a641ebf221 | |||
40d0f50838 | |||
2385f3c9db | |||
b51c608d3f | |||
a3be70f0a5 | |||
7f187f9857 | |||
4036448c02 | |||
94d7523f83 | |||
f7786e207e | |||
9e212c515e | |||
ad9872d884 | |||
90757544fd | |||
302daddcd1 | |||
a278e925a1 | |||
a8413a1539 | |||
078229a5e4 | |||
44a1f90308 | |||
b222ec7631 | |||
f136cca5df | |||
7cabc5670d | |||
ea9051624d | |||
0d37f3a79b | |||
7624b78544 | |||
f1afbb3442 | |||
1e0642b086 | |||
d7ce77b7c6 | |||
1d894dda24 | |||
d0bfd3e523 | |||
a1dfe07bfc | |||
6d2f645363 | |||
c647e8639f | |||
e7fcf3f189 | |||
f23fd221e4 | |||
dc39043cd6 |
|
@ -4,6 +4,7 @@ reportUnusedDisableDirectives: true
|
|||
ignorePatterns:
|
||||
- /web_src/js/vendor
|
||||
- /web_src/fomantic
|
||||
- /public/assets/js
|
||||
|
||||
parserOptions:
|
||||
sourceType: module
|
||||
|
|
|
@ -22,7 +22,7 @@ jobs:
|
|||
|
||||
runs-on: docker
|
||||
container:
|
||||
image: ghcr.io/visualon/renovate:37.316.2
|
||||
image: ghcr.io/visualon/renovate:37.323.3
|
||||
|
||||
steps:
|
||||
- name: Load renovate repo cache
|
||||
|
|
|
@ -4,21 +4,4 @@ The Forgejo project is run by a community of people who are expected to follow t
|
|||
|
||||
Sensitive security-related issues should be reported to [security@forgejo.org](mailto:security@forgejo.org) using [encryption](https://keyoxide.org/security@forgejo.org).
|
||||
|
||||
## For everyone involved
|
||||
|
||||
- [Documentation](https://forgejo.org/docs/next/)
|
||||
- [Code of Conduct](https://forgejo.org/docs/latest/developer/coc/)
|
||||
- [Bugs, features, security and others discussions](https://forgejo.org/docs/latest/developer/discussions/)
|
||||
- [Governance](https://forgejo.org/docs/latest/developer/governance/)
|
||||
- [Sustainability and funding](https://codeberg.org/forgejo/sustainability/src/branch/main/README.md)
|
||||
|
||||
## For contributors
|
||||
|
||||
- [Developer Certificate of Origin (DCO)](https://forgejo.org/docs/latest/developer/dco/)
|
||||
- [Development workflow](https://forgejo.org/docs/latest/developer/workflow/)
|
||||
- [Compiling from source](https://forgejo.org/docs/latest/developer/from-source/)
|
||||
|
||||
## For maintainers
|
||||
|
||||
- [Release management](https://forgejo.org/docs/latest/developer/release/)
|
||||
- [Secrets](https://forgejo.org/docs/latest/developer/secrets/)
|
||||
You can find links to the different aspects of Developer documentation on this page: [Forgejo developer guide](https://forgejo.org/docs/next/developer/).
|
||||
|
|
|
@ -4,28 +4,9 @@ A minor or major Forgejo release is published every [three months](https://forge
|
|||
|
||||
A [patch or minor release](https://semver.org/spec/v2.0.0.html) (e.g. upgrading from v7.0.0 to v7.0.1 or v7.1.0) does not require manual intervention. But [major releases](https://semver.org/spec/v2.0.0.html#spec-item-8) where the first version number changes (e.g. upgrading from v1.21 to v7.0) contain breaking changes and the release notes explain how to deal with them.
|
||||
|
||||
## 8.0.0
|
||||
## Upcoming releases (not available yet)
|
||||
|
||||
This is a major release. It contains breaking changes that may require manual intervention. See the documentation for more information on the [upgrade procedure](https://forgejo.org/docs/v7.0/admin/upgrade/).
|
||||
|
||||
* **Breaking changes:**
|
||||
|
||||
In addition to the following notable bug fixes, you can browse the [full list of commits](https://codeberg.org/forgejo/forgejo/compare/v8.0.0...v7.0.0) included in this release.
|
||||
|
||||
If you have any feedback or suggestions for Forgejo do not hold back, it is also your project.
|
||||
Open an issue in [the issue tracker](https://codeberg.org/forgejo/forgejo/issues)
|
||||
for feature requests or bug reports, reach out [on the Fediverse](https://floss.social/@forgejo),
|
||||
or drop into [the Matrix space](https://matrix.to/#/#forgejo:matrix.org)
|
||||
([main chat room](https://matrix.to/#/#forgejo-chat:matrix.org)) and say hi!
|
||||
|
||||
## 7.0.1
|
||||
|
||||
This is a bug fix release. See the documentation for more information on the [upgrade procedure](https://forgejo.org/docs/v7.0/admin/upgrade/).
|
||||
|
||||
In addition to the following notable bug fixes, you can browse the [full list of commits](https://codeberg.org/forgejo/forgejo/compare/v7.0.0...v7.0.1) included in this release.
|
||||
|
||||
* **Bug fixes:**
|
||||
* The regression in the [`fogejo admin user create`](https://forgejo.org/docs/v7.0/admin/command-line/#admin-user-create) CLI command [is fixed](https://codeberg.org/forgejo/forgejo/issues/3399) and it is backward compatible.
|
||||
- [8.0.0](/release-notes/8.0.0/)
|
||||
|
||||
## 7.0.0
|
||||
|
||||
|
@ -36,7 +17,8 @@ $ git clone https://codeberg.org/forgejo/forgejo/
|
|||
$ git -C forgejo log --oneline --no-merges origin/v1.21/forgejo..origin/v7.0/forgejo
|
||||
```
|
||||
|
||||
* **Regression and workaround:**
|
||||
* **Regressions and workarounds:**
|
||||
* Running the [`forgejo doctor check --fix`](https://forgejo.org/docs/v7.0/admin/command-line/#doctor-check) CLI command or setting [`[cron.gc_lfs].ENABLED=true`](https://forgejo.org/docs/v7.0/admin/config-cheat-sheet/#cron---garbage-collect-lfs-pointers-in-repositories-crongc_lfs) (the default is `false`) will corrupt the LFS storage. The workaround is to not run the doctor CLI command and disable the `cron.gc_lfs`. This regression will be [fixed in 7.0.1](https://codeberg.org/forgejo/forgejo/issues/3438).
|
||||
* The [`fogejo admin user create`](https://forgejo.org/docs/v7.0/admin/command-line/#admin-user-create) CLI command [requires a password](https://codeberg.org/forgejo/forgejo/commit/b122c6ef8b9254120432aed373cbe075331132ac) change by default when creating the first user and the `--admin` flag is not specified. The `--must-change-password=false` argument must be given to not require a password change. This regression will be [fixed in 7.0.1](https://codeberg.org/forgejo/forgejo/issues/3399).
|
||||
* **Breaking changes requiring manual intervention:**
|
||||
* [MySQL 8.0 or PostgreSQL 12](https://codeberg.org/forgejo/forgejo/commit/e94f9fcafdcf284561e7fb33f60156a69c4ad6a5) are the minimum supported versions. The database must be migrated before upgrading. The requirements regarding SQLite did not change.
|
||||
|
|
|
@ -407,8 +407,8 @@ USER = root
|
|||
;; Database connection max life time, default is 0 or 3s mysql (See #6804 & #7071 for reasoning)
|
||||
;CONN_MAX_LIFETIME = 3s
|
||||
;;
|
||||
;; Database maximum number of open connections, default is 0 meaning no maximum
|
||||
;MAX_OPEN_CONNS = 0
|
||||
;; Database maximum number of open connections, default is 100 which is the lowest default from Postgres (MariaDB + MySQL default to 151). Ensure you only increase the value if you configured your database server accordingly.
|
||||
;MAX_OPEN_CONNS = 100
|
||||
;;
|
||||
;; Whether execute database models migrations automatically
|
||||
;AUTO_MIGRATION = true
|
||||
|
|
|
@ -33,6 +33,7 @@ const (
|
|||
DLDAP // 5
|
||||
OAuth2 // 6
|
||||
SSPI // 7
|
||||
Remote // 8
|
||||
)
|
||||
|
||||
// String returns the string name of the LoginType
|
||||
|
@ -53,6 +54,7 @@ var Names = map[Type]string{
|
|||
PAM: "PAM",
|
||||
OAuth2: "OAuth2",
|
||||
SSPI: "SPNEGO with SSPI",
|
||||
Remote: "Remote",
|
||||
}
|
||||
|
||||
// Config represents login config as far as the db is concerned
|
||||
|
@ -181,6 +183,10 @@ func (source *Source) IsSSPI() bool {
|
|||
return source.Type == SSPI
|
||||
}
|
||||
|
||||
func (source *Source) IsRemote() bool {
|
||||
return source.Type == Remote
|
||||
}
|
||||
|
||||
// HasTLS returns true of this source supports TLS.
|
||||
func (source *Source) HasTLS() bool {
|
||||
hasTLSer, ok := source.Cfg.(HasTLSer)
|
||||
|
|
|
@ -9,7 +9,7 @@
|
|||
|
||||
-
|
||||
|
||||
id: 2
|
||||
id: 2 # this is an LFS orphan object
|
||||
oid: 2eccdb43825d2a49d99d542daa20075cff1d97d9d2349a8977efe9c03661737c
|
||||
size: 107
|
||||
repository_id: 54
|
||||
|
|
|
@ -64,6 +64,8 @@ var migrations = []*Migration{
|
|||
NewMigration("Add repo_archive_download_count table", forgejo_v1_22.AddRepoArchiveDownloadCount),
|
||||
// v13 -> v14
|
||||
NewMigration("Add `hide_archive_links` column to `release` table", AddHideArchiveLinksToRelease),
|
||||
// v14 -> v15
|
||||
NewMigration("Remove Gitea-specific columns from the repository and badge tables", RemoveGiteaSpecificColumnsFromRepositoryAndBadge),
|
||||
}
|
||||
|
||||
// GetCurrentDBVersion returns the current Forgejo database version.
|
||||
|
|
43
models/forgejo_migrations/v14.go
Normal file
43
models/forgejo_migrations/v14.go
Normal file
|
@ -0,0 +1,43 @@
|
|||
// Copyright 2024 The Forgejo Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package forgejo_migrations //nolint:revive
|
||||
|
||||
import (
|
||||
"code.gitea.io/gitea/models/migrations/base"
|
||||
|
||||
"xorm.io/xorm"
|
||||
)
|
||||
|
||||
func RemoveGiteaSpecificColumnsFromRepositoryAndBadge(x *xorm.Engine) error {
|
||||
// Make sure the columns exist before dropping them
|
||||
type Repository struct {
|
||||
ID int64
|
||||
DefaultWikiBranch string
|
||||
}
|
||||
if err := x.Sync(&Repository{}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
type Badge struct {
|
||||
ID int64 `xorm:"pk autoincr"`
|
||||
Slug string
|
||||
}
|
||||
err := x.Sync(new(Badge))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
sess := x.NewSession()
|
||||
defer sess.Close()
|
||||
if err := sess.Begin(); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := base.DropTableColumns(sess, "repository", "default_wiki_branch"); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := base.DropTableColumns(sess, "badge", "slug"); err != nil {
|
||||
return err
|
||||
}
|
||||
return sess.Commit()
|
||||
}
|
36
models/user/fixtures/user.yml
Normal file
36
models/user/fixtures/user.yml
Normal file
|
@ -0,0 +1,36 @@
|
|||
-
|
||||
id: 1041
|
||||
lower_name: remote01
|
||||
name: remote01
|
||||
full_name: Remote01
|
||||
email: remote01@example.com
|
||||
keep_email_private: false
|
||||
email_notifications_preference: onmention
|
||||
passwd: ZogKvWdyEx:password
|
||||
passwd_hash_algo: dummy
|
||||
must_change_password: false
|
||||
login_source: 1001
|
||||
login_name: 123
|
||||
type: 5
|
||||
salt: ZogKvWdyEx
|
||||
max_repo_creation: -1
|
||||
is_active: true
|
||||
is_admin: false
|
||||
is_restricted: false
|
||||
allow_git_hook: false
|
||||
allow_import_local: false
|
||||
allow_create_organization: true
|
||||
prohibit_login: true
|
||||
avatar: avatarremote01
|
||||
avatar_email: avatarremote01@example.com
|
||||
use_custom_avatar: false
|
||||
num_followers: 0
|
||||
num_following: 0
|
||||
num_stars: 0
|
||||
num_repos: 0
|
||||
num_teams: 0
|
||||
num_members: 0
|
||||
visibility: 0
|
||||
repo_admin_change_team_access: false
|
||||
theme: ""
|
||||
keep_activity_private: false
|
|
@ -45,7 +45,11 @@ type SearchUserOptions struct {
|
|||
|
||||
func (opts *SearchUserOptions) toSearchQueryBase(ctx context.Context) *xorm.Session {
|
||||
var cond builder.Cond
|
||||
cond = builder.Eq{"type": opts.Type}
|
||||
if opts.Type == UserTypeIndividual {
|
||||
cond = builder.In("type", UserTypeIndividual, UserTypeRemoteUser)
|
||||
} else {
|
||||
cond = builder.Eq{"type": opts.Type}
|
||||
}
|
||||
if opts.IncludeReserved {
|
||||
if opts.Type == UserTypeIndividual {
|
||||
cond = cond.Or(builder.Eq{"type": UserTypeUserReserved}).Or(
|
||||
|
|
|
@ -216,7 +216,7 @@ func (u *User) GetEmail() string {
|
|||
// GetAllUsers returns a slice of all individual users found in DB.
|
||||
func GetAllUsers(ctx context.Context) ([]*User, error) {
|
||||
users := make([]*User, 0)
|
||||
return users, db.GetEngine(ctx).OrderBy("id").Where("type = ?", UserTypeIndividual).Find(&users)
|
||||
return users, db.GetEngine(ctx).OrderBy("id").In("type", UserTypeIndividual, UserTypeRemoteUser).Find(&users)
|
||||
}
|
||||
|
||||
// GetAllAdmins returns a slice of all adminusers found in DB.
|
||||
|
@ -416,6 +416,10 @@ func (u *User) IsBot() bool {
|
|||
return u.Type == UserTypeBot
|
||||
}
|
||||
|
||||
func (u *User) IsRemote() bool {
|
||||
return u.Type == UserTypeRemoteUser
|
||||
}
|
||||
|
||||
// DisplayName returns full name if it's not empty,
|
||||
// returns username otherwise.
|
||||
func (u *User) DisplayName() string {
|
||||
|
@ -918,7 +922,8 @@ func GetUserByName(ctx context.Context, name string) (*User, error) {
|
|||
if len(name) == 0 {
|
||||
return nil, ErrUserNotExist{Name: name}
|
||||
}
|
||||
u := &User{LowerName: strings.ToLower(name), Type: UserTypeIndividual}
|
||||
// adding Type: UserTypeIndividual is a noop because it is zero and discarded
|
||||
u := &User{LowerName: strings.ToLower(name)}
|
||||
has, err := db.GetEngine(ctx).Get(u)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
|
|
@ -21,6 +21,7 @@ import (
|
|||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/modules/structs"
|
||||
"code.gitea.io/gitea/modules/timeutil"
|
||||
"code.gitea.io/gitea/tests"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
@ -33,6 +34,35 @@ func TestOAuth2Application_LoadUser(t *testing.T) {
|
|||
assert.NotNil(t, user)
|
||||
}
|
||||
|
||||
func TestGetUserByName(t *testing.T) {
|
||||
defer tests.AddFixtures("models/user/fixtures/")()
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
|
||||
{
|
||||
_, err := user_model.GetUserByName(db.DefaultContext, "")
|
||||
assert.True(t, user_model.IsErrUserNotExist(err), err)
|
||||
}
|
||||
{
|
||||
_, err := user_model.GetUserByName(db.DefaultContext, "UNKNOWN")
|
||||
assert.True(t, user_model.IsErrUserNotExist(err), err)
|
||||
}
|
||||
{
|
||||
user, err := user_model.GetUserByName(db.DefaultContext, "USER2")
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, user.Name, "user2")
|
||||
}
|
||||
{
|
||||
user, err := user_model.GetUserByName(db.DefaultContext, "org3")
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, user.Name, "org3")
|
||||
}
|
||||
{
|
||||
user, err := user_model.GetUserByName(db.DefaultContext, "remote01")
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, user.Name, "remote01")
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetUserEmailsByNames(t *testing.T) {
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
|
||||
|
@ -61,7 +91,24 @@ func TestCanCreateOrganization(t *testing.T) {
|
|||
assert.False(t, user.CanCreateOrganization())
|
||||
}
|
||||
|
||||
func TestGetAllUsers(t *testing.T) {
|
||||
defer tests.AddFixtures("models/user/fixtures/")()
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
|
||||
users, err := user_model.GetAllUsers(db.DefaultContext)
|
||||
assert.NoError(t, err)
|
||||
|
||||
found := make(map[user_model.UserType]bool, 0)
|
||||
for _, user := range users {
|
||||
found[user.Type] = true
|
||||
}
|
||||
assert.True(t, found[user_model.UserTypeIndividual], users)
|
||||
assert.True(t, found[user_model.UserTypeRemoteUser], users)
|
||||
assert.False(t, found[user_model.UserTypeOrganization], users)
|
||||
}
|
||||
|
||||
func TestSearchUsers(t *testing.T) {
|
||||
defer tests.AddFixtures("models/user/fixtures/")()
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
testSuccess := func(opts *user_model.SearchUserOptions, expectedUserOrOrgIDs []int64) {
|
||||
users, _, err := user_model.SearchUsers(db.DefaultContext, opts)
|
||||
|
@ -102,13 +149,13 @@ func TestSearchUsers(t *testing.T) {
|
|||
}
|
||||
|
||||
testUserSuccess(&user_model.SearchUserOptions{OrderBy: "id ASC", ListOptions: db.ListOptions{Page: 1}},
|
||||
[]int64{1, 2, 4, 5, 8, 9, 10, 11, 12, 13, 14, 15, 16, 18, 20, 21, 24, 27, 28, 29, 30, 32, 34, 37, 38, 39, 40})
|
||||
[]int64{1, 2, 4, 5, 8, 9, 10, 11, 12, 13, 14, 15, 16, 18, 20, 21, 24, 27, 28, 29, 30, 32, 34, 37, 38, 39, 40, 1041})
|
||||
|
||||
testUserSuccess(&user_model.SearchUserOptions{ListOptions: db.ListOptions{Page: 1}, IsActive: optional.Some(false)},
|
||||
[]int64{9})
|
||||
|
||||
testUserSuccess(&user_model.SearchUserOptions{OrderBy: "id ASC", ListOptions: db.ListOptions{Page: 1}, IsActive: optional.Some(true)},
|
||||
[]int64{1, 2, 4, 5, 8, 10, 11, 12, 13, 14, 15, 16, 18, 20, 21, 24, 27, 28, 29, 30, 32, 34, 37, 38, 39, 40})
|
||||
[]int64{1, 2, 4, 5, 8, 10, 11, 12, 13, 14, 15, 16, 18, 20, 21, 24, 27, 28, 29, 30, 32, 34, 37, 38, 39, 40, 1041})
|
||||
|
||||
testUserSuccess(&user_model.SearchUserOptions{Keyword: "user1", OrderBy: "id ASC", ListOptions: db.ListOptions{Page: 1}, IsActive: optional.Some(true)},
|
||||
[]int64{1, 10, 11, 12, 13, 14, 15, 16, 18})
|
||||
|
@ -124,7 +171,7 @@ func TestSearchUsers(t *testing.T) {
|
|||
[]int64{29})
|
||||
|
||||
testUserSuccess(&user_model.SearchUserOptions{ListOptions: db.ListOptions{Page: 1}, IsProhibitLogin: optional.Some(true)},
|
||||
[]int64{37})
|
||||
[]int64{1041, 37})
|
||||
|
||||
testUserSuccess(&user_model.SearchUserOptions{ListOptions: db.ListOptions{Page: 1}, IsTwoFactorEnabled: optional.Some(true)},
|
||||
[]int64{24})
|
||||
|
|
|
@ -6,6 +6,7 @@ package git
|
|||
import (
|
||||
"crypto/sha1"
|
||||
"crypto/sha256"
|
||||
"hash"
|
||||
"regexp"
|
||||
"strconv"
|
||||
)
|
||||
|
@ -33,6 +34,15 @@ type ObjectFormat interface {
|
|||
ComputeHash(t ObjectType, content []byte) ObjectID
|
||||
}
|
||||
|
||||
func computeHash(dst []byte, hasher hash.Hash, t ObjectType, content []byte) []byte {
|
||||
_, _ = hasher.Write(t.Bytes())
|
||||
_, _ = hasher.Write([]byte(" "))
|
||||
_, _ = hasher.Write([]byte(strconv.Itoa(len(content))))
|
||||
_, _ = hasher.Write([]byte{0})
|
||||
_, _ = hasher.Write(content)
|
||||
return hasher.Sum(dst)
|
||||
}
|
||||
|
||||
/* SHA1 Type */
|
||||
type Sha1ObjectFormatImpl struct{}
|
||||
|
||||
|
@ -65,16 +75,9 @@ func (Sha1ObjectFormatImpl) MustID(b []byte) ObjectID {
|
|||
|
||||
// ComputeHash compute the hash for a given ObjectType and content
|
||||
func (h Sha1ObjectFormatImpl) ComputeHash(t ObjectType, content []byte) ObjectID {
|
||||
hasher := sha1.New()
|
||||
_, _ = hasher.Write(t.Bytes())
|
||||
_, _ = hasher.Write([]byte(" "))
|
||||
_, _ = hasher.Write([]byte(strconv.FormatInt(int64(len(content)), 10)))
|
||||
_, _ = hasher.Write([]byte{0})
|
||||
|
||||
// HashSum generates a SHA1 for the provided hash
|
||||
var sha1 Sha1Hash
|
||||
copy(sha1[:], hasher.Sum(nil))
|
||||
return &sha1
|
||||
var obj Sha1Hash
|
||||
computeHash(obj[:0], sha1.New(), t, content)
|
||||
return &obj
|
||||
}
|
||||
|
||||
/* SHA256 Type */
|
||||
|
@ -111,16 +114,9 @@ func (Sha256ObjectFormatImpl) MustID(b []byte) ObjectID {
|
|||
|
||||
// ComputeHash compute the hash for a given ObjectType and content
|
||||
func (h Sha256ObjectFormatImpl) ComputeHash(t ObjectType, content []byte) ObjectID {
|
||||
hasher := sha256.New()
|
||||
_, _ = hasher.Write(t.Bytes())
|
||||
_, _ = hasher.Write([]byte(" "))
|
||||
_, _ = hasher.Write([]byte(strconv.FormatInt(int64(len(content)), 10)))
|
||||
_, _ = hasher.Write([]byte{0})
|
||||
|
||||
// HashSum generates a SHA256 for the provided hash
|
||||
var sha256 Sha1Hash
|
||||
copy(sha256[:], hasher.Sum(nil))
|
||||
return &sha256
|
||||
var obj Sha256Hash
|
||||
computeHash(obj[:0], sha256.New(), t, content)
|
||||
return &obj
|
||||
}
|
||||
|
||||
var (
|
||||
|
|
|
@ -18,4 +18,8 @@ func TestIsValidSHAPattern(t *testing.T) {
|
|||
assert.False(t, h.IsValid("abc"))
|
||||
assert.False(t, h.IsValid("123g"))
|
||||
assert.False(t, h.IsValid("some random text"))
|
||||
|
||||
assert.Equal(t, "79ee38a6416c1ede423ec7ee0a8639ceea4aad22", ComputeBlobHash(Sha1ObjectFormat, []byte("some random blob")).String())
|
||||
assert.Equal(t, "d5c6407415d85df49592672aa421aed39b9db5e3", ComputeBlobHash(Sha1ObjectFormat, []byte("same length blob")).String())
|
||||
assert.Equal(t, "df0b5174ed06ae65aea40d43316bcbc21d82c9e3158ce2661df2ad28d7931dd6", ComputeBlobHash(Sha256ObjectFormat, []byte("some random blob")).String())
|
||||
}
|
||||
|
|
32
modules/git/pipeline/lfs_common.go
Normal file
32
modules/git/pipeline/lfs_common.go
Normal file
|
@ -0,0 +1,32 @@
|
|||
// Copyright 2024 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package pipeline
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"code.gitea.io/gitea/modules/git"
|
||||
)
|
||||
|
||||
// LFSResult represents commits found using a provided pointer file hash
|
||||
type LFSResult struct {
|
||||
Name string
|
||||
SHA string
|
||||
Summary string
|
||||
When time.Time
|
||||
ParentHashes []git.ObjectID
|
||||
BranchName string
|
||||
FullCommitName string
|
||||
}
|
||||
|
||||
type lfsResultSlice []*LFSResult
|
||||
|
||||
func (a lfsResultSlice) Len() int { return len(a) }
|
||||
func (a lfsResultSlice) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
|
||||
func (a lfsResultSlice) Less(i, j int) bool { return a[j].When.After(a[i].When) }
|
||||
|
||||
func lfsError(msg string, err error) error {
|
||||
return fmt.Errorf("LFS error occurred, %s: err: %w", msg, err)
|
||||
}
|
|
@ -7,12 +7,10 @@ package pipeline
|
|||
|
||||
import (
|
||||
"bufio"
|
||||
"fmt"
|
||||
"io"
|
||||
"sort"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"code.gitea.io/gitea/modules/git"
|
||||
|
||||
|
@ -21,23 +19,6 @@ import (
|
|||
"github.com/go-git/go-git/v5/plumbing/object"
|
||||
)
|
||||
|
||||
// LFSResult represents commits found using a provided pointer file hash
|
||||
type LFSResult struct {
|
||||
Name string
|
||||
SHA string
|
||||
Summary string
|
||||
When time.Time
|
||||
ParentHashes []git.ObjectID
|
||||
BranchName string
|
||||
FullCommitName string
|
||||
}
|
||||
|
||||
type lfsResultSlice []*LFSResult
|
||||
|
||||
func (a lfsResultSlice) Len() int { return len(a) }
|
||||
func (a lfsResultSlice) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
|
||||
func (a lfsResultSlice) Less(i, j int) bool { return a[j].When.After(a[i].When) }
|
||||
|
||||
// FindLFSFile finds commits that contain a provided pointer file hash
|
||||
func FindLFSFile(repo *git.Repository, objectID git.ObjectID) ([]*LFSResult, error) {
|
||||
resultsMap := map[string]*LFSResult{}
|
||||
|
@ -51,7 +32,7 @@ func FindLFSFile(repo *git.Repository, objectID git.ObjectID) ([]*LFSResult, err
|
|||
All: true,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Failed to get GoGit CommitsIter. Error: %w", err)
|
||||
return nil, lfsError("failed to get GoGit CommitsIter", err)
|
||||
}
|
||||
|
||||
err = commitsIter.ForEach(func(gitCommit *object.Commit) error {
|
||||
|
@ -85,7 +66,7 @@ func FindLFSFile(repo *git.Repository, objectID git.ObjectID) ([]*LFSResult, err
|
|||
return nil
|
||||
})
|
||||
if err != nil && err != io.EOF {
|
||||
return nil, fmt.Errorf("Failure in CommitIter.ForEach: %w", err)
|
||||
return nil, lfsError("failure in CommitIter.ForEach", err)
|
||||
}
|
||||
|
||||
for _, result := range resultsMap {
|
||||
|
@ -156,7 +137,7 @@ func FindLFSFile(repo *git.Repository, objectID git.ObjectID) ([]*LFSResult, err
|
|||
select {
|
||||
case err, has := <-errChan:
|
||||
if has {
|
||||
return nil, fmt.Errorf("Unable to obtain name for LFS files. Error: %w", err)
|
||||
return nil, lfsError("unable to obtain name for LFS files", err)
|
||||
}
|
||||
default:
|
||||
}
|
|
@ -8,33 +8,14 @@ package pipeline
|
|||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"sort"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"code.gitea.io/gitea/modules/git"
|
||||
)
|
||||
|
||||
// LFSResult represents commits found using a provided pointer file hash
|
||||
type LFSResult struct {
|
||||
Name string
|
||||
SHA string
|
||||
Summary string
|
||||
When time.Time
|
||||
ParentIDs []git.ObjectID
|
||||
BranchName string
|
||||
FullCommitName string
|
||||
}
|
||||
|
||||
type lfsResultSlice []*LFSResult
|
||||
|
||||
func (a lfsResultSlice) Len() int { return len(a) }
|
||||
func (a lfsResultSlice) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
|
||||
func (a lfsResultSlice) Less(i, j int) bool { return a[j].When.After(a[i].When) }
|
||||
|
||||
// FindLFSFile finds commits that contain a provided pointer file hash
|
||||
func FindLFSFile(repo *git.Repository, objectID git.ObjectID) ([]*LFSResult, error) {
|
||||
resultsMap := map[string]*LFSResult{}
|
||||
|
@ -137,11 +118,11 @@ func FindLFSFile(repo *git.Repository, objectID git.ObjectID) ([]*LFSResult, err
|
|||
n += int64(count)
|
||||
if bytes.Equal(binObjectID, objectID.RawValue()) {
|
||||
result := LFSResult{
|
||||
Name: curPath + string(fname),
|
||||
SHA: curCommit.ID.String(),
|
||||
Summary: strings.Split(strings.TrimSpace(curCommit.CommitMessage), "\n")[0],
|
||||
When: curCommit.Author.When,
|
||||
ParentIDs: curCommit.Parents,
|
||||
Name: curPath + string(fname),
|
||||
SHA: curCommit.ID.String(),
|
||||
Summary: strings.Split(strings.TrimSpace(curCommit.CommitMessage), "\n")[0],
|
||||
When: curCommit.Author.When,
|
||||
ParentHashes: curCommit.Parents,
|
||||
}
|
||||
resultsMap[curCommit.ID.String()+":"+curPath+string(fname)] = &result
|
||||
} else if string(mode) == git.EntryModeTree.String() {
|
||||
|
@ -183,7 +164,7 @@ func FindLFSFile(repo *git.Repository, objectID git.ObjectID) ([]*LFSResult, err
|
|||
|
||||
for _, result := range resultsMap {
|
||||
hasParent := false
|
||||
for _, parentID := range result.ParentIDs {
|
||||
for _, parentID := range result.ParentHashes {
|
||||
if _, hasParent = resultsMap[parentID.String()+":"+result.Name]; hasParent {
|
||||
break
|
||||
}
|
||||
|
@ -241,7 +222,7 @@ func FindLFSFile(repo *git.Repository, objectID git.ObjectID) ([]*LFSResult, err
|
|||
select {
|
||||
case err, has := <-errChan:
|
||||
if has {
|
||||
return nil, fmt.Errorf("Unable to obtain name for LFS files. Error: %w", err)
|
||||
return nil, lfsError("unable to obtain name for LFS files", err)
|
||||
}
|
||||
default:
|
||||
}
|
||||
|
|
|
@ -41,6 +41,8 @@ const (
|
|||
maxBatchSize = 16
|
||||
// fuzzyDenominator determines the levenshtein distance per each character of a keyword
|
||||
fuzzyDenominator = 4
|
||||
// see https://github.com/blevesearch/bleve/issues/1563#issuecomment-786822311
|
||||
maxFuzziness = 2
|
||||
)
|
||||
|
||||
func addUnicodeNormalizeTokenFilter(m *mapping.IndexMappingImpl) error {
|
||||
|
@ -246,7 +248,7 @@ func (b *Indexer) Search(ctx context.Context, opts *internal.SearchOptions) (int
|
|||
phraseQuery.Analyzer = repoIndexerAnalyzer
|
||||
keywordQuery = phraseQuery
|
||||
if opts.IsKeywordFuzzy {
|
||||
phraseQuery.Fuzziness = len(opts.Keyword) / fuzzyDenominator
|
||||
phraseQuery.Fuzziness = min(maxFuzziness, len(opts.Keyword)/fuzzyDenominator)
|
||||
}
|
||||
|
||||
if len(opts.RepoIDs) > 0 {
|
||||
|
|
|
@ -49,6 +49,12 @@ func testIndexer(name string, t *testing.T, indexer internal.Indexer) {
|
|||
IDs: []int64{},
|
||||
Langs: 0,
|
||||
},
|
||||
{
|
||||
RepoIDs: nil,
|
||||
Keyword: "Description for",
|
||||
IDs: []int64{repoID},
|
||||
Langs: 1,
|
||||
},
|
||||
{
|
||||
RepoIDs: nil,
|
||||
Keyword: "repo1",
|
||||
|
|
|
@ -39,6 +39,8 @@ const (
|
|||
maxBatchSize = 16
|
||||
// fuzzyDenominator determines the levenshtein distance per each character of a keyword
|
||||
fuzzyDenominator = 4
|
||||
// see https://github.com/blevesearch/bleve/issues/1563#issuecomment-786822311
|
||||
maxFuzziness = 2
|
||||
)
|
||||
|
||||
// IndexerData an update to the issue indexer
|
||||
|
@ -162,7 +164,7 @@ func (b *Indexer) Search(ctx context.Context, options *internal.SearchOptions) (
|
|||
if options.Keyword != "" {
|
||||
fuzziness := 0
|
||||
if options.IsFuzzyKeyword {
|
||||
fuzziness = len(options.Keyword) / fuzzyDenominator
|
||||
fuzziness = min(maxFuzziness, len(options.Keyword)/fuzzyDenominator)
|
||||
}
|
||||
|
||||
queries = append(queries, bleve.NewDisjunctionQuery([]query.Query{
|
||||
|
|
|
@ -130,6 +130,20 @@ var cases = []*testIndexerCase{
|
|||
ExpectedIDs: []int64{1002, 1001, 1000},
|
||||
ExpectedTotal: 3,
|
||||
},
|
||||
{
|
||||
Name: "Keyword Fuzzy",
|
||||
ExtraData: []*internal.IndexerData{
|
||||
{ID: 1000, Title: "hi hello world"},
|
||||
{ID: 1001, Content: "hi hello world"},
|
||||
{ID: 1002, Comments: []string{"hi", "hello world"}},
|
||||
},
|
||||
SearchOptions: &internal.SearchOptions{
|
||||
Keyword: "hello wrold",
|
||||
IsFuzzyKeyword: true,
|
||||
},
|
||||
ExpectedIDs: []int64{1002, 1001, 1000},
|
||||
ExpectedTotal: 3,
|
||||
},
|
||||
{
|
||||
Name: "RepoIDs",
|
||||
ExtraData: []*internal.IndexerData{
|
||||
|
|
|
@ -83,7 +83,7 @@ func loadDBSetting(rootCfg ConfigProvider) {
|
|||
Database.ConnMaxLifetime = sec.Key("CONN_MAX_LIFETIME").MustDuration(0)
|
||||
}
|
||||
Database.ConnMaxIdleTime = sec.Key("CONN_MAX_IDLETIME").MustDuration(0)
|
||||
Database.MaxOpenConns = sec.Key("MAX_OPEN_CONNS").MustInt(0)
|
||||
Database.MaxOpenConns = sec.Key("MAX_OPEN_CONNS").MustInt(100)
|
||||
|
||||
Database.IterateBufferSize = sec.Key("ITERATE_BUFFER_SIZE").MustInt(50)
|
||||
Database.LogSQL = sec.Key("LOG_SQL").MustBool(false)
|
||||
|
|
|
@ -162,7 +162,7 @@ var (
|
|||
PreferredLicenses: []string{"Apache-2.0", "MIT"},
|
||||
DisableHTTPGit: false,
|
||||
AccessControlAllowOrigin: "",
|
||||
UseCompatSSHURI: false,
|
||||
UseCompatSSHURI: true,
|
||||
DefaultCloseIssuesViaCommitsInAnyBranch: false,
|
||||
EnablePushCreateUser: false,
|
||||
EnablePushCreateOrg: false,
|
||||
|
|
74
package-lock.json
generated
74
package-lock.json
generated
|
@ -44,7 +44,7 @@
|
|||
"postcss-nesting": "12.1.2",
|
||||
"pretty-ms": "9.0.0",
|
||||
"sortablejs": "1.15.2",
|
||||
"swagger-ui-dist": "5.17.0",
|
||||
"swagger-ui-dist": "5.17.2",
|
||||
"tailwindcss": "3.4.3",
|
||||
"temporal-polyfill": "0.2.4",
|
||||
"throttle-debounce": "5.0.0",
|
||||
|
@ -96,7 +96,7 @@
|
|||
"svgo": "3.2.0",
|
||||
"updates": "16.0.1",
|
||||
"vite-string-plugin": "1.2.0",
|
||||
"vitest": "1.5.0"
|
||||
"vitest": "1.5.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 18.0.0"
|
||||
|
@ -2545,13 +2545,13 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@vitest/expect": {
|
||||
"version": "1.5.0",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-1.5.0.tgz",
|
||||
"integrity": "sha512-0pzuCI6KYi2SIC3LQezmxujU9RK/vwC1U9R0rLuGlNGcOuDWxqWKu6nUdFsX9tH1WU0SXtAxToOsEjeUn1s3hA==",
|
||||
"version": "1.5.2",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-1.5.2.tgz",
|
||||
"integrity": "sha512-rf7MTD1WCoDlN3FfYJ9Llfp0PbdtOMZ3FIF0AVkDnKbp3oiMW1c8AmvRZBcqbAhDUAvF52e9zx4WQM1r3oraVA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@vitest/spy": "1.5.0",
|
||||
"@vitest/utils": "1.5.0",
|
||||
"@vitest/spy": "1.5.2",
|
||||
"@vitest/utils": "1.5.2",
|
||||
"chai": "^4.3.10"
|
||||
},
|
||||
"funding": {
|
||||
|
@ -2559,12 +2559,12 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@vitest/runner": {
|
||||
"version": "1.5.0",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-1.5.0.tgz",
|
||||
"integrity": "sha512-7HWwdxXP5yDoe7DTpbif9l6ZmDwCzcSIK38kTSIt6CFEpMjX4EpCgT6wUmS0xTXqMI6E/ONmfgRKmaujpabjZQ==",
|
||||
"version": "1.5.2",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-1.5.2.tgz",
|
||||
"integrity": "sha512-7IJ7sJhMZrqx7HIEpv3WrMYcq8ZNz9L6alo81Y6f8hV5mIE6yVZsFoivLZmr0D777klm1ReqonE9LyChdcmw6g==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@vitest/utils": "1.5.0",
|
||||
"@vitest/utils": "1.5.2",
|
||||
"p-limit": "^5.0.0",
|
||||
"pathe": "^1.1.1"
|
||||
},
|
||||
|
@ -2600,9 +2600,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@vitest/snapshot": {
|
||||
"version": "1.5.0",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-1.5.0.tgz",
|
||||
"integrity": "sha512-qpv3fSEuNrhAO3FpH6YYRdaECnnRjg9VxbhdtPwPRnzSfHVXnNzzrpX4cJxqiwgRMo7uRMWDFBlsBq4Cr+rO3A==",
|
||||
"version": "1.5.2",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-1.5.2.tgz",
|
||||
"integrity": "sha512-CTEp/lTYos8fuCc9+Z55Ga5NVPKUgExritjF5VY7heRFUfheoAqBneUlvXSUJHUZPjnPmyZA96yLRJDP1QATFQ==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"magic-string": "^0.30.5",
|
||||
|
@ -2626,9 +2626,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@vitest/spy": {
|
||||
"version": "1.5.0",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-1.5.0.tgz",
|
||||
"integrity": "sha512-vu6vi6ew5N5MMHJjD5PoakMRKYdmIrNJmyfkhRpQt5d9Ewhw9nZ5Aqynbi3N61bvk9UvZ5UysMT6ayIrZ8GA9w==",
|
||||
"version": "1.5.2",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-1.5.2.tgz",
|
||||
"integrity": "sha512-xCcPvI8JpCtgikT9nLpHPL1/81AYqZy1GCy4+MCHBE7xi8jgsYkULpW5hrx5PGLgOQjUpb6fd15lqcriJ40tfQ==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"tinyspy": "^2.2.0"
|
||||
|
@ -2638,9 +2638,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@vitest/utils": {
|
||||
"version": "1.5.0",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-1.5.0.tgz",
|
||||
"integrity": "sha512-BDU0GNL8MWkRkSRdNFvCUCAVOeHaUlVJ9Tx0TYBZyXaaOTmGtUFObzchCivIBrIwKzvZA7A9sCejVhXM2aY98A==",
|
||||
"version": "1.5.2",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-1.5.2.tgz",
|
||||
"integrity": "sha512-sWOmyofuXLJ85VvXNsroZur7mOJGiQeM0JN3/0D1uU8U9bGFM69X1iqHaRXl6R8BwaLY6yPCogP257zxTzkUdA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"diff-sequences": "^29.6.3",
|
||||
|
@ -11495,9 +11495,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/swagger-ui-dist": {
|
||||
"version": "5.17.0",
|
||||
"resolved": "https://registry.npmjs.org/swagger-ui-dist/-/swagger-ui-dist-5.17.0.tgz",
|
||||
"integrity": "sha512-PtEozc87rN6i6rqLYNVTK+1ZAYmCMy6poU6I2MOJXD19BVv6D7U9zwS8geRbtfamCM5yUwWkSNQKWGK58vculg=="
|
||||
"version": "5.17.2",
|
||||
"resolved": "https://registry.npmjs.org/swagger-ui-dist/-/swagger-ui-dist-5.17.2.tgz",
|
||||
"integrity": "sha512-V/NqUw6QoTrjSpctp2oLQvxrl3vW29UsUtZyq7B1CF0v870KOFbYGDQw8rpKaKm0JxTwHpWnW1SN9YuKZdiCyw=="
|
||||
},
|
||||
"node_modules/sync-fetch": {
|
||||
"version": "0.4.5",
|
||||
|
@ -12257,9 +12257,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/vite-node": {
|
||||
"version": "1.5.0",
|
||||
"resolved": "https://registry.npmjs.org/vite-node/-/vite-node-1.5.0.tgz",
|
||||
"integrity": "sha512-tV8h6gMj6vPzVCa7l+VGq9lwoJjW8Y79vst8QZZGiuRAfijU+EEWuc0kFpmndQrWhMMhet1jdSF+40KSZUqIIw==",
|
||||
"version": "1.5.2",
|
||||
"resolved": "https://registry.npmjs.org/vite-node/-/vite-node-1.5.2.tgz",
|
||||
"integrity": "sha512-Y8p91kz9zU+bWtF7HGt6DVw2JbhyuB2RlZix3FPYAYmUyZ3n7iTp8eSyLyY6sxtPegvxQtmlTMhfPhUfCUF93A==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"cac": "^6.7.14",
|
||||
|
@ -12339,16 +12339,16 @@
|
|||
}
|
||||
},
|
||||
"node_modules/vitest": {
|
||||
"version": "1.5.0",
|
||||
"resolved": "https://registry.npmjs.org/vitest/-/vitest-1.5.0.tgz",
|
||||
"integrity": "sha512-d8UKgR0m2kjdxDWX6911uwxout6GHS0XaGH1cksSIVVG8kRlE7G7aBw7myKQCvDI5dT4j7ZMa+l706BIORMDLw==",
|
||||
"version": "1.5.2",
|
||||
"resolved": "https://registry.npmjs.org/vitest/-/vitest-1.5.2.tgz",
|
||||
"integrity": "sha512-l9gwIkq16ug3xY7BxHwcBQovLZG75zZL0PlsiYQbf76Rz6QGs54416UWMtC0jXeihvHvcHrf2ROEjkQRVpoZYw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@vitest/expect": "1.5.0",
|
||||
"@vitest/runner": "1.5.0",
|
||||
"@vitest/snapshot": "1.5.0",
|
||||
"@vitest/spy": "1.5.0",
|
||||
"@vitest/utils": "1.5.0",
|
||||
"@vitest/expect": "1.5.2",
|
||||
"@vitest/runner": "1.5.2",
|
||||
"@vitest/snapshot": "1.5.2",
|
||||
"@vitest/spy": "1.5.2",
|
||||
"@vitest/utils": "1.5.2",
|
||||
"acorn-walk": "^8.3.2",
|
||||
"chai": "^4.3.10",
|
||||
"debug": "^4.3.4",
|
||||
|
@ -12362,7 +12362,7 @@
|
|||
"tinybench": "^2.5.1",
|
||||
"tinypool": "^0.8.3",
|
||||
"vite": "^5.0.0",
|
||||
"vite-node": "1.5.0",
|
||||
"vite-node": "1.5.2",
|
||||
"why-is-node-running": "^2.2.2"
|
||||
},
|
||||
"bin": {
|
||||
|
@ -12377,8 +12377,8 @@
|
|||
"peerDependencies": {
|
||||
"@edge-runtime/vm": "*",
|
||||
"@types/node": "^18.0.0 || >=20.0.0",
|
||||
"@vitest/browser": "1.5.0",
|
||||
"@vitest/ui": "1.5.0",
|
||||
"@vitest/browser": "1.5.2",
|
||||
"@vitest/ui": "1.5.2",
|
||||
"happy-dom": "*",
|
||||
"jsdom": "*"
|
||||
},
|
||||
|
|
|
@ -43,7 +43,7 @@
|
|||
"postcss-nesting": "12.1.2",
|
||||
"pretty-ms": "9.0.0",
|
||||
"sortablejs": "1.15.2",
|
||||
"swagger-ui-dist": "5.17.0",
|
||||
"swagger-ui-dist": "5.17.2",
|
||||
"tailwindcss": "3.4.3",
|
||||
"temporal-polyfill": "0.2.4",
|
||||
"throttle-debounce": "5.0.0",
|
||||
|
@ -95,7 +95,7 @@
|
|||
"svgo": "3.2.0",
|
||||
"updates": "16.0.1",
|
||||
"vite-string-plugin": "1.2.0",
|
||||
"vitest": "1.5.0"
|
||||
"vitest": "1.5.2"
|
||||
},
|
||||
"browserslist": ["defaults"]
|
||||
}
|
||||
|
|
1
release-notes/8.0.0/feat/3434.md
Normal file
1
release-notes/8.0.0/feat/3434.md
Normal file
|
@ -0,0 +1 @@
|
|||
When PDFs are displayed in the repository, the [full height of the screen](https://codeberg.org/forgejo/forgejo/pulls/3434) is now used instead of a predefined fixed height
|
1
release-notes/8.0.0/fix/3399.md
Normal file
1
release-notes/8.0.0/fix/3399.md
Normal file
|
@ -0,0 +1 @@
|
|||
The regression in the [`fogejo admin user create`](https://forgejo.org/docs/v7.0/admin/command-line/#admin-user-create) CLI command [is fixed](https://codeberg.org/forgejo/forgejo/issues/3399) and it is backward compatible.
|
1
release-notes/8.0.0/fix/3430.md
Normal file
1
release-notes/8.0.0/fix/3430.md
Normal file
|
@ -0,0 +1 @@
|
|||
Fixed a bug where the `/api/v1/repos/{owner}/{repo}/wiki` API endpoints were using a hardcoded "master" branch for the wiki, rather than the branch they really use.
|
1
release-notes/8.0.0/fix/3442.md
Normal file
1
release-notes/8.0.0/fix/3442.md
Normal file
|
@ -0,0 +1 @@
|
|||
Save updated empty comments instead of skipping the update silently, [which prevented the removal of attachments of such comments](https://codeberg.org/forgejo/forgejo/issues/3424).
|
1
release-notes/8.0.0/fix/3444.md
Normal file
1
release-notes/8.0.0/fix/3444.md
Normal file
|
@ -0,0 +1 @@
|
|||
Fixed bleve indexer failing when [fuzziness exceeds the maximum 2](https://codeberg.org/forgejo/forgejo/pulls/3444)
|
1
release-notes/8.0.0/fix/3451.md
Normal file
1
release-notes/8.0.0/fix/3451.md
Normal file
|
@ -0,0 +1 @@
|
|||
Fixed an error 500 when visiting [the LFS settings](https://codeberg.org/forgejo/forgejo/pulls/3451) at `/{owner}/{repo}/settings/lfs/find?oid=...`.
|
|
@ -68,6 +68,21 @@
|
|||
"matchUpdateTypes": ["minor", "patch", "digest"],
|
||||
"automerge": true
|
||||
},
|
||||
{
|
||||
"description": "Split minor and patch updates",
|
||||
"matchDepNames": [
|
||||
"swagger-ui-dist"
|
||||
],
|
||||
"separateMinorPatch": true
|
||||
},
|
||||
{
|
||||
"description": "Automerge patch updates",
|
||||
"matchDepNames": [
|
||||
"swagger-ui-dist"
|
||||
],
|
||||
"matchUpdateTypes": ["patch"],
|
||||
"automerge": true
|
||||
},
|
||||
{
|
||||
"description": "Update renovate with higher prio to come through rate limit",
|
||||
"matchDatasources": ["docker"],
|
||||
|
|
|
@ -193,7 +193,7 @@ func getWikiPage(ctx *context.APIContext, wikiName wiki_service.WebPath) *api.Wi
|
|||
}
|
||||
|
||||
// get commit count - wiki revisions
|
||||
commitsCount, _ := wikiRepo.FileCommitsCount("master", pageFilename)
|
||||
commitsCount, _ := wikiRepo.FileCommitsCount(ctx.Repo.Repository.GetWikiBranchName(), pageFilename)
|
||||
|
||||
// Get last change information.
|
||||
lastCommit, err := wikiRepo.GetCommitByPath(pageFilename)
|
||||
|
@ -432,7 +432,7 @@ func ListPageRevisions(ctx *context.APIContext) {
|
|||
}
|
||||
|
||||
// get commit count - wiki revisions
|
||||
commitsCount, _ := wikiRepo.FileCommitsCount("master", pageFilename)
|
||||
commitsCount, _ := wikiRepo.FileCommitsCount(ctx.Repo.Repository.GetWikiBranchName(), pageFilename)
|
||||
|
||||
page := ctx.FormInt("page")
|
||||
if page <= 1 {
|
||||
|
@ -442,7 +442,7 @@ func ListPageRevisions(ctx *context.APIContext) {
|
|||
// get Commit Count
|
||||
commitsHistory, err := wikiRepo.CommitsByFileAndRange(
|
||||
git.CommitsByFileAndRangeOptions{
|
||||
Revision: "master",
|
||||
Revision: ctx.Repo.Repository.GetWikiBranchName(),
|
||||
File: pageFilename,
|
||||
Page: page,
|
||||
})
|
||||
|
@ -487,7 +487,7 @@ func findWikiRepoCommit(ctx *context.APIContext) (*git.Repository, *git.Commit)
|
|||
return nil, nil
|
||||
}
|
||||
|
||||
commit, err := wikiRepo.GetBranchCommit("master")
|
||||
commit, err := wikiRepo.GetBranchCommit(ctx.Repo.Repository.GetWikiBranchName())
|
||||
if err != nil {
|
||||
if git.IsErrNotExist(err) {
|
||||
ctx.NotFound(err)
|
||||
|
|
|
@ -35,6 +35,7 @@ import (
|
|||
"code.gitea.io/gitea/services/context"
|
||||
"code.gitea.io/gitea/services/externalaccount"
|
||||
"code.gitea.io/gitea/services/forms"
|
||||
remote_service "code.gitea.io/gitea/services/remote"
|
||||
user_service "code.gitea.io/gitea/services/user"
|
||||
|
||||
"gitea.com/go-chi/binding"
|
||||
|
@ -1202,9 +1203,21 @@ func handleOAuth2SignIn(ctx *context.Context, source *auth.Source, u *user_model
|
|||
ctx.Redirect(setting.AppSubURL + "/user/two_factor")
|
||||
}
|
||||
|
||||
// OAuth2UserLoginCallback attempts to handle the callback from the OAuth2 provider and if successful
|
||||
// login the user
|
||||
func oAuth2UserLoginCallback(ctx *context.Context, authSource *auth.Source, request *http.Request, response http.ResponseWriter) (*user_model.User, goth.User, error) {
|
||||
gothUser, err := oAuth2FetchUser(ctx, authSource, request, response)
|
||||
if err != nil {
|
||||
return nil, goth.User{}, err
|
||||
}
|
||||
|
||||
if _, _, err := remote_service.MaybePromoteRemoteUser(ctx, authSource, gothUser.UserID, gothUser.Email); err != nil {
|
||||
return nil, goth.User{}, err
|
||||
}
|
||||
|
||||
u, err := oAuth2GothUserToUser(request.Context(), authSource, gothUser)
|
||||
return u, gothUser, err
|
||||
}
|
||||
|
||||
func oAuth2FetchUser(ctx *context.Context, authSource *auth.Source, request *http.Request, response http.ResponseWriter) (goth.User, error) {
|
||||
oauth2Source := authSource.Cfg.(*oauth2.Source)
|
||||
|
||||
// Make sure that the response is not an error response.
|
||||
|
@ -1216,10 +1229,10 @@ func oAuth2UserLoginCallback(ctx *context.Context, authSource *auth.Source, requ
|
|||
// Delete the goth session
|
||||
err := gothic.Logout(response, request)
|
||||
if err != nil {
|
||||
return nil, goth.User{}, err
|
||||
return goth.User{}, err
|
||||
}
|
||||
|
||||
return nil, goth.User{}, errCallback{
|
||||
return goth.User{}, errCallback{
|
||||
Code: errorName,
|
||||
Description: errorDescription,
|
||||
}
|
||||
|
@ -1232,24 +1245,28 @@ func oAuth2UserLoginCallback(ctx *context.Context, authSource *auth.Source, requ
|
|||
log.Error("OAuth2 Provider %s returned too long a token. Current max: %d. Either increase the [OAuth2] MAX_TOKEN_LENGTH or reduce the information returned from the OAuth2 provider", authSource.Name, setting.OAuth2.MaxTokenLength)
|
||||
err = fmt.Errorf("OAuth2 Provider %s returned too long a token. Current max: %d. Either increase the [OAuth2] MAX_TOKEN_LENGTH or reduce the information returned from the OAuth2 provider", authSource.Name, setting.OAuth2.MaxTokenLength)
|
||||
}
|
||||
return nil, goth.User{}, err
|
||||
return goth.User{}, err
|
||||
}
|
||||
|
||||
if oauth2Source.RequiredClaimName != "" {
|
||||
claimInterface, has := gothUser.RawData[oauth2Source.RequiredClaimName]
|
||||
if !has {
|
||||
return nil, goth.User{}, user_model.ErrUserProhibitLogin{Name: gothUser.UserID}
|
||||
return goth.User{}, user_model.ErrUserProhibitLogin{Name: gothUser.UserID}
|
||||
}
|
||||
|
||||
if oauth2Source.RequiredClaimValue != "" {
|
||||
groups := claimValueToStringSet(claimInterface)
|
||||
|
||||
if !groups.Contains(oauth2Source.RequiredClaimValue) {
|
||||
return nil, goth.User{}, user_model.ErrUserProhibitLogin{Name: gothUser.UserID}
|
||||
return goth.User{}, user_model.ErrUserProhibitLogin{Name: gothUser.UserID}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return gothUser, nil
|
||||
}
|
||||
|
||||
func oAuth2GothUserToUser(ctx go_context.Context, authSource *auth.Source, gothUser goth.User) (*user_model.User, error) {
|
||||
user := &user_model.User{
|
||||
LoginName: gothUser.UserID,
|
||||
LoginType: auth.OAuth2,
|
||||
|
@ -1258,27 +1275,28 @@ func oAuth2UserLoginCallback(ctx *context.Context, authSource *auth.Source, requ
|
|||
|
||||
hasUser, err := user_model.GetUser(ctx, user)
|
||||
if err != nil {
|
||||
return nil, goth.User{}, err
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if hasUser {
|
||||
return user, gothUser, nil
|
||||
return user, nil
|
||||
}
|
||||
log.Debug("no user found for LoginName %v, LoginSource %v, LoginType %v", user.LoginName, user.LoginSource, user.LoginType)
|
||||
|
||||
// search in external linked users
|
||||
externalLoginUser := &user_model.ExternalLoginUser{
|
||||
ExternalID: gothUser.UserID,
|
||||
LoginSourceID: authSource.ID,
|
||||
}
|
||||
hasUser, err = user_model.GetExternalLogin(request.Context(), externalLoginUser)
|
||||
hasUser, err = user_model.GetExternalLogin(ctx, externalLoginUser)
|
||||
if err != nil {
|
||||
return nil, goth.User{}, err
|
||||
return nil, err
|
||||
}
|
||||
if hasUser {
|
||||
user, err = user_model.GetUserByID(request.Context(), externalLoginUser.UserID)
|
||||
return user, gothUser, err
|
||||
user, err = user_model.GetUserByID(ctx, externalLoginUser.UserID)
|
||||
return user, err
|
||||
}
|
||||
|
||||
// no user found to login
|
||||
return nil, gothUser, nil
|
||||
return nil, nil
|
||||
}
|
||||
|
|
|
@ -3160,12 +3160,6 @@ func UpdateCommentContent(ctx *context.Context) {
|
|||
|
||||
oldContent := comment.Content
|
||||
comment.Content = ctx.FormString("content")
|
||||
if len(comment.Content) == 0 {
|
||||
ctx.JSON(http.StatusOK, map[string]any{
|
||||
"content": "",
|
||||
})
|
||||
return
|
||||
}
|
||||
if err = issue_service.UpdateComment(ctx, comment, ctx.Doer, oldContent); err != nil {
|
||||
ctx.ServerError("UpdateComment", err)
|
||||
return
|
||||
|
|
33
services/auth/source/remote/source.go
Normal file
33
services/auth/source/remote/source.go
Normal file
|
@ -0,0 +1,33 @@
|
|||
// Copyright Earl Warren <contact@earl-warren.org>
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package remote
|
||||
|
||||
import (
|
||||
"code.gitea.io/gitea/models/auth"
|
||||
"code.gitea.io/gitea/modules/json"
|
||||
)
|
||||
|
||||
type Source struct {
|
||||
URL string
|
||||
MatchingSource string
|
||||
|
||||
// reference to the authSource
|
||||
authSource *auth.Source
|
||||
}
|
||||
|
||||
func (source *Source) FromDB(bs []byte) error {
|
||||
return json.UnmarshalHandleDoubleEncode(bs, &source)
|
||||
}
|
||||
|
||||
func (source *Source) ToDB() ([]byte, error) {
|
||||
return json.Marshal(source)
|
||||
}
|
||||
|
||||
func (source *Source) SetAuthSource(authSource *auth.Source) {
|
||||
source.authSource = authSource
|
||||
}
|
||||
|
||||
func init() {
|
||||
auth.RegisterTypeConfig(auth.Remote, &Source{})
|
||||
}
|
|
@ -237,6 +237,7 @@ func innerToRepo(ctx context.Context, repo *repo_model.Repository, permissionInR
|
|||
MirrorInterval: mirrorInterval,
|
||||
MirrorUpdated: mirrorUpdated,
|
||||
RepoTransfer: transfer,
|
||||
ObjectFormatName: repo.ObjectFormatName,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
133
services/remote/promote.go
Normal file
133
services/remote/promote.go
Normal file
|
@ -0,0 +1,133 @@
|
|||
// Copyright Earl Warren <contact@earl-warren.org>
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package remote
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
auth_model "code.gitea.io/gitea/models/auth"
|
||||
"code.gitea.io/gitea/models/db"
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
"code.gitea.io/gitea/services/auth/source/oauth2"
|
||||
remote_source "code.gitea.io/gitea/services/auth/source/remote"
|
||||
)
|
||||
|
||||
type Reason int
|
||||
|
||||
const (
|
||||
ReasonNoMatch Reason = iota
|
||||
ReasonNotAuth2
|
||||
ReasonBadAuth2
|
||||
ReasonLoginNameNotExists
|
||||
ReasonNotRemote
|
||||
ReasonEmailIsSet
|
||||
ReasonNoSource
|
||||
ReasonSourceWrongType
|
||||
ReasonCanPromote
|
||||
ReasonPromoted
|
||||
ReasonUpdateFail
|
||||
ReasonErrorLoginName
|
||||
ReasonErrorGetSource
|
||||
)
|
||||
|
||||
func NewReason(level log.Level, reason Reason, message string, args ...any) Reason {
|
||||
log.Log(1, level, message, args...)
|
||||
return reason
|
||||
}
|
||||
|
||||
func getUsersByLoginName(ctx context.Context, name string) ([]*user_model.User, error) {
|
||||
if len(name) == 0 {
|
||||
return nil, user_model.ErrUserNotExist{Name: name}
|
||||
}
|
||||
|
||||
users := make([]*user_model.User, 0, 5)
|
||||
|
||||
return users, db.GetEngine(ctx).
|
||||
Table("user").
|
||||
Where("login_name = ? AND login_type = ? AND type = ?", name, auth_model.Remote, user_model.UserTypeRemoteUser).
|
||||
Find(&users)
|
||||
}
|
||||
|
||||
// The remote user has:
|
||||
//
|
||||
// Type UserTypeRemoteUser
|
||||
// LogingType Remote
|
||||
// LoginName set to the unique identifier of the originating authentication source
|
||||
// LoginSource set to the Remote source that can be matched against an OAuth2 source
|
||||
//
|
||||
// If the source from which an authentification happens is OAuth2, an existing
|
||||
// remote user will be promoted to an OAuth2 user provided:
|
||||
//
|
||||
// user.LoginName is the same as goth.UserID (argument loginName)
|
||||
// user.LoginSource has a MatchingSource equals to the name of the OAuth2 provider
|
||||
//
|
||||
// Once promoted, the user will be logged in without further interaction from the
|
||||
// user and will own all repositories, issues, etc. associated with it.
|
||||
func MaybePromoteRemoteUser(ctx context.Context, source *auth_model.Source, loginName, email string) (promoted bool, reason Reason, err error) {
|
||||
user, reason, err := getRemoteUserToPromote(ctx, source, loginName, email)
|
||||
if err != nil || user == nil {
|
||||
return false, reason, err
|
||||
}
|
||||
promote := &user_model.User{
|
||||
ID: user.ID,
|
||||
Type: user_model.UserTypeIndividual,
|
||||
Email: email,
|
||||
LoginSource: source.ID,
|
||||
LoginType: source.Type,
|
||||
}
|
||||
reason = NewReason(log.DEBUG, ReasonPromoted, "promote user %v: LoginName %v => %v, LoginSource %v => %v, LoginType %v => %v, Email %v => %v", user.ID, user.LoginName, promote.LoginName, user.LoginSource, promote.LoginSource, user.LoginType, promote.LoginType, user.Email, promote.Email)
|
||||
if err := user_model.UpdateUserCols(ctx, promote, "type", "email", "login_source", "login_type"); err != nil {
|
||||
return false, ReasonUpdateFail, err
|
||||
}
|
||||
return true, reason, nil
|
||||
}
|
||||
|
||||
func getRemoteUserToPromote(ctx context.Context, source *auth_model.Source, loginName, email string) (*user_model.User, Reason, error) {
|
||||
if !source.IsOAuth2() {
|
||||
return nil, NewReason(log.DEBUG, ReasonNotAuth2, "source %v is not OAuth2", source), nil
|
||||
}
|
||||
oauth2Source, ok := source.Cfg.(*oauth2.Source)
|
||||
if !ok {
|
||||
return nil, NewReason(log.ERROR, ReasonBadAuth2, "source claims to be OAuth2 but is not"), nil
|
||||
}
|
||||
|
||||
users, err := getUsersByLoginName(ctx, loginName)
|
||||
if err != nil {
|
||||
return nil, NewReason(log.ERROR, ReasonErrorLoginName, "getUserByLoginName('%s') %v", loginName, err), err
|
||||
}
|
||||
if len(users) == 0 {
|
||||
return nil, NewReason(log.ERROR, ReasonLoginNameNotExists, "no user with LoginType UserTypeRemoteUser and LoginName '%s'", loginName), nil
|
||||
}
|
||||
|
||||
reason := ReasonNoSource
|
||||
for _, u := range users {
|
||||
userSource, err := auth_model.GetSourceByID(ctx, u.LoginSource)
|
||||
if err != nil {
|
||||
if auth_model.IsErrSourceNotExist(err) {
|
||||
reason = NewReason(log.DEBUG, ReasonNoSource, "source id = %v for user %v not found %v", u.LoginSource, u.ID, err)
|
||||
continue
|
||||
}
|
||||
return nil, NewReason(log.ERROR, ReasonErrorGetSource, "GetSourceByID('%s') %v", u.LoginSource, err), err
|
||||
}
|
||||
if u.Email != "" {
|
||||
reason = NewReason(log.DEBUG, ReasonEmailIsSet, "the user email is already set to '%s'", u.Email)
|
||||
continue
|
||||
}
|
||||
remoteSource, ok := userSource.Cfg.(*remote_source.Source)
|
||||
if !ok {
|
||||
reason = NewReason(log.DEBUG, ReasonSourceWrongType, "expected a remote source but got %T %v", userSource, userSource)
|
||||
continue
|
||||
}
|
||||
|
||||
if oauth2Source.Provider != remoteSource.MatchingSource {
|
||||
reason = NewReason(log.DEBUG, ReasonNoMatch, "skip OAuth2 source %s because it is different from %s which is the expected match for the remote source %s", oauth2Source.Provider, remoteSource.MatchingSource, remoteSource.URL)
|
||||
continue
|
||||
}
|
||||
|
||||
return u, ReasonCanPromote, nil
|
||||
}
|
||||
|
||||
return nil, reason, nil
|
||||
}
|
|
@ -28,9 +28,13 @@ func TestGarbageCollectLFSMetaObjects(t *testing.T) {
|
|||
err := storage.Init()
|
||||
assert.NoError(t, err)
|
||||
|
||||
repo, err := repo_model.GetRepositoryByOwnerAndName(db.DefaultContext, "user2", "repo1")
|
||||
repo, err := repo_model.GetRepositoryByOwnerAndName(db.DefaultContext, "user2", "lfs")
|
||||
assert.NoError(t, err)
|
||||
|
||||
validLFSObjects, err := db.GetEngine(db.DefaultContext).Count(git_model.LFSMetaObject{RepositoryID: repo.ID})
|
||||
assert.NoError(t, err)
|
||||
assert.Greater(t, validLFSObjects, int64(1))
|
||||
|
||||
// add lfs object
|
||||
lfsContent := []byte("gitea1")
|
||||
lfsOid := storeObjectInRepo(t, repo.ID, &lfsContent)
|
||||
|
@ -39,13 +43,18 @@ func TestGarbageCollectLFSMetaObjects(t *testing.T) {
|
|||
err = repo_service.GarbageCollectLFSMetaObjects(context.Background(), repo_service.GarbageCollectLFSMetaObjectsOptions{
|
||||
AutoFix: true,
|
||||
OlderThan: time.Now().Add(7 * 24 * time.Hour).Add(5 * 24 * time.Hour),
|
||||
UpdatedLessRecentlyThan: time.Now().Add(7 * 24 * time.Hour).Add(3 * 24 * time.Hour),
|
||||
UpdatedLessRecentlyThan: time.Time{}, // ensure that the models/fixtures/lfs_meta_object.yml objects are considered as well
|
||||
LogDetail: t.Logf,
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
|
||||
// lfs meta has been deleted
|
||||
_, err = git_model.GetLFSMetaObjectByOid(db.DefaultContext, repo.ID, lfsOid)
|
||||
assert.ErrorIs(t, err, git_model.ErrLFSObjectNotExist)
|
||||
|
||||
remainingLFSObjects, err := db.GetEngine(db.DefaultContext).Count(git_model.LFSMetaObject{RepositoryID: repo.ID})
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, validLFSObjects-1, remainingLFSObjects)
|
||||
}
|
||||
|
||||
func storeObjectInRepo(t *testing.T, repositoryID int64, content *[]byte) string {
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
{{with .Repository}}
|
||||
<div class="ui container">
|
||||
<div class="repo-header">
|
||||
<div class="flex-item gt-ac">
|
||||
<div class="flex-item tw-items-center">
|
||||
<div class="flex-item-leading">
|
||||
{{template "repo/icon" .}}
|
||||
</div>
|
||||
|
|
|
@ -701,3 +701,14 @@ func TestAPIRepoGetAssignees(t *testing.T) {
|
|||
DecodeJSON(t, resp, &assignees)
|
||||
assert.Len(t, assignees, 1)
|
||||
}
|
||||
|
||||
func TestAPIViewRepoObjectFormat(t *testing.T) {
|
||||
defer tests.PrepareTestEnv(t)()
|
||||
|
||||
var repo api.Repository
|
||||
|
||||
req := NewRequest(t, "GET", "/api/v1/repos/user2/repo1")
|
||||
resp := MakeRequest(t, req, http.StatusOK)
|
||||
DecodeJSON(t, resp, &repo)
|
||||
assert.EqualValues(t, "sha1", repo.ObjectFormatName)
|
||||
}
|
||||
|
|
|
@ -16,6 +16,7 @@ import (
|
|||
unit_model "code.gitea.io/gitea/models/unit"
|
||||
"code.gitea.io/gitea/models/unittest"
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
"code.gitea.io/gitea/modules/optional"
|
||||
api "code.gitea.io/gitea/modules/structs"
|
||||
repo_service "code.gitea.io/gitea/services/repository"
|
||||
"code.gitea.io/gitea/tests"
|
||||
|
@ -382,3 +383,28 @@ func TestAPIListPageRevisions(t *testing.T) {
|
|||
|
||||
assert.Equal(t, dummyrevisions, revisions)
|
||||
}
|
||||
|
||||
func TestAPIWikiNonMasterBranch(t *testing.T) {
|
||||
defer tests.PrepareTestEnv(t)()
|
||||
|
||||
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1})
|
||||
repo, _, f := CreateDeclarativeRepoWithOptions(t, user, DeclarativeRepoOptions{
|
||||
WikiBranch: optional.Some("main"),
|
||||
})
|
||||
defer f()
|
||||
|
||||
uris := []string{
|
||||
"revisions/Home",
|
||||
"pages",
|
||||
"page/Home",
|
||||
}
|
||||
baseURL := fmt.Sprintf("/api/v1/repos/%s/wiki", repo.FullName())
|
||||
for _, uri := range uris {
|
||||
t.Run(uri, func(t *testing.T) {
|
||||
defer tests.PrintCurrentTest(t)()
|
||||
|
||||
req := NewRequestf(t, "GET", "%s/%s", baseURL, uri)
|
||||
MakeRequest(t, req, http.StatusOK)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -36,22 +36,26 @@ import (
|
|||
"code.gitea.io/gitea/modules/graceful"
|
||||
"code.gitea.io/gitea/modules/json"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
"code.gitea.io/gitea/modules/optional"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/modules/testlogger"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
"code.gitea.io/gitea/modules/web"
|
||||
"code.gitea.io/gitea/routers"
|
||||
"code.gitea.io/gitea/services/auth/source/remote"
|
||||
gitea_context "code.gitea.io/gitea/services/context"
|
||||
repo_service "code.gitea.io/gitea/services/repository"
|
||||
files_service "code.gitea.io/gitea/services/repository/files"
|
||||
user_service "code.gitea.io/gitea/services/user"
|
||||
wiki_service "code.gitea.io/gitea/services/wiki"
|
||||
"code.gitea.io/gitea/tests"
|
||||
|
||||
"github.com/PuerkitoBio/goquery"
|
||||
gouuid "github.com/google/uuid"
|
||||
"github.com/markbates/goth"
|
||||
"github.com/markbates/goth/gothic"
|
||||
goth_gitlab "github.com/markbates/goth/providers/gitlab"
|
||||
goth_gitlab "github.com/markbates/goth/providers/github"
|
||||
goth_github "github.com/markbates/goth/providers/gitlab"
|
||||
"github.com/santhosh-tekuri/jsonschema/v5"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
@ -336,6 +340,36 @@ func authSourcePayloadGitLabCustom(name string) map[string]string {
|
|||
return payload
|
||||
}
|
||||
|
||||
func authSourcePayloadGitHub(name string) map[string]string {
|
||||
payload := authSourcePayloadOAuth2(name)
|
||||
payload["oauth2_provider"] = "github"
|
||||
return payload
|
||||
}
|
||||
|
||||
func authSourcePayloadGitHubCustom(name string) map[string]string {
|
||||
payload := authSourcePayloadGitHub(name)
|
||||
payload["oauth2_use_custom_url"] = "on"
|
||||
payload["oauth2_auth_url"] = goth_github.AuthURL
|
||||
payload["oauth2_token_url"] = goth_github.TokenURL
|
||||
payload["oauth2_profile_url"] = goth_github.ProfileURL
|
||||
return payload
|
||||
}
|
||||
|
||||
func createRemoteAuthSource(t *testing.T, name, url, matchingSource string) *auth.Source {
|
||||
assert.NoError(t, auth.CreateSource(context.Background(), &auth.Source{
|
||||
Type: auth.Remote,
|
||||
Name: name,
|
||||
IsActive: true,
|
||||
Cfg: &remote.Source{
|
||||
URL: url,
|
||||
MatchingSource: matchingSource,
|
||||
},
|
||||
}))
|
||||
source, err := auth.GetSourceByName(context.Background(), name)
|
||||
assert.NoError(t, err)
|
||||
return source
|
||||
}
|
||||
|
||||
func createUser(ctx context.Context, t testing.TB, user *user_model.User) func() {
|
||||
user.MustChangePassword = false
|
||||
user.LowerName = strings.ToLower(user.Name)
|
||||
|
@ -652,15 +686,27 @@ func GetHTMLTitle(t testing.TB, session *TestSession, urlStr string) string {
|
|||
return doc.Find("head title").Text()
|
||||
}
|
||||
|
||||
func CreateDeclarativeRepo(t *testing.T, owner *user_model.User, name string, enabledUnits, disabledUnits []unit_model.Type, files []*files_service.ChangeRepoFile) (*repo_model.Repository, string, func()) {
|
||||
type DeclarativeRepoOptions struct {
|
||||
Name optional.Option[string]
|
||||
EnabledUnits optional.Option[[]unit_model.Type]
|
||||
DisabledUnits optional.Option[[]unit_model.Type]
|
||||
Files optional.Option[[]*files_service.ChangeRepoFile]
|
||||
WikiBranch optional.Option[string]
|
||||
}
|
||||
|
||||
func CreateDeclarativeRepoWithOptions(t *testing.T, owner *user_model.User, opts DeclarativeRepoOptions) (*repo_model.Repository, string, func()) {
|
||||
t.Helper()
|
||||
|
||||
repoName := name
|
||||
if repoName == "" {
|
||||
// Not using opts.Name.ValueOrDefault() here to avoid unnecessarily
|
||||
// generating an UUID when a name is specified.
|
||||
var repoName string
|
||||
if opts.Name.Has() {
|
||||
repoName = opts.Name.Value()
|
||||
} else {
|
||||
repoName = gouuid.NewString()
|
||||
}
|
||||
|
||||
// Create a new repository
|
||||
// Create the repository
|
||||
repo, err := repo_service.CreateRepository(db.DefaultContext, owner, owner, repo_service.CreateRepoOptions{
|
||||
Name: repoName,
|
||||
Description: "Temporary Repo",
|
||||
|
@ -673,21 +719,31 @@ func CreateDeclarativeRepo(t *testing.T, owner *user_model.User, name string, en
|
|||
assert.NoError(t, err)
|
||||
assert.NotEmpty(t, repo)
|
||||
|
||||
if enabledUnits != nil || disabledUnits != nil {
|
||||
units := make([]repo_model.RepoUnit, len(enabledUnits))
|
||||
for i, unitType := range enabledUnits {
|
||||
units[i] = repo_model.RepoUnit{
|
||||
// Populate `enabledUnits` if we have any enabled.
|
||||
var enabledUnits []repo_model.RepoUnit
|
||||
if opts.EnabledUnits.Has() {
|
||||
units := opts.EnabledUnits.Value()
|
||||
enabledUnits = make([]repo_model.RepoUnit, len(units))
|
||||
|
||||
for i, unitType := range units {
|
||||
enabledUnits[i] = repo_model.RepoUnit{
|
||||
RepoID: repo.ID,
|
||||
Type: unitType,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
err := repo_service.UpdateRepositoryUnits(db.DefaultContext, repo, units, disabledUnits)
|
||||
// Adjust the repo units according to our parameters.
|
||||
if opts.EnabledUnits.Has() || opts.DisabledUnits.Has() {
|
||||
err := repo_service.UpdateRepositoryUnits(db.DefaultContext, repo, enabledUnits, opts.DisabledUnits.ValueOrDefault(nil))
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
// Add files, if any.
|
||||
var sha string
|
||||
if len(files) > 0 {
|
||||
if opts.Files.Has() {
|
||||
files := opts.Files.Value()
|
||||
|
||||
resp, err := files_service.ChangeRepoFiles(git.DefaultContext, repo, owner, &files_service.ChangeRepoFilesOptions{
|
||||
Files: files,
|
||||
Message: "add files",
|
||||
|
@ -712,7 +768,46 @@ func CreateDeclarativeRepo(t *testing.T, owner *user_model.User, name string, en
|
|||
sha = resp.Commit.SHA
|
||||
}
|
||||
|
||||
// If there's a Wiki branch specified, create a wiki, and a default wiki page.
|
||||
if opts.WikiBranch.Has() {
|
||||
// Set the wiki branch in the database first
|
||||
repo.WikiBranch = opts.WikiBranch.Value()
|
||||
err := repo_model.UpdateRepositoryCols(db.DefaultContext, repo, "wiki_branch")
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Initialize the wiki
|
||||
err = wiki_service.InitWiki(db.DefaultContext, repo)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Add a new wiki page
|
||||
err = wiki_service.AddWikiPage(db.DefaultContext, owner, repo, "Home", "Welcome to the wiki!", "Add a Home page")
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
// Return the repo, the top commit, and a defer-able function to delete the
|
||||
// repo.
|
||||
return repo, sha, func() {
|
||||
repo_service.DeleteRepository(db.DefaultContext, owner, repo, false)
|
||||
}
|
||||
}
|
||||
|
||||
func CreateDeclarativeRepo(t *testing.T, owner *user_model.User, name string, enabledUnits, disabledUnits []unit_model.Type, files []*files_service.ChangeRepoFile) (*repo_model.Repository, string, func()) {
|
||||
t.Helper()
|
||||
|
||||
var opts DeclarativeRepoOptions
|
||||
|
||||
if name != "" {
|
||||
opts.Name = optional.Some(name)
|
||||
}
|
||||
if enabledUnits != nil {
|
||||
opts.EnabledUnits = optional.Some(enabledUnits)
|
||||
}
|
||||
if disabledUnits != nil {
|
||||
opts.DisabledUnits = optional.Some(disabledUnits)
|
||||
}
|
||||
if files != nil {
|
||||
opts.Files = optional.Some(files)
|
||||
}
|
||||
|
||||
return CreateDeclarativeRepoWithOptions(t, owner, opts)
|
||||
}
|
||||
|
|
|
@ -307,6 +307,16 @@ func TestIssueCommentUpdate(t *testing.T) {
|
|||
|
||||
comment = unittest.AssertExistsAndLoadBean(t, &issues_model.Comment{ID: commentID})
|
||||
assert.Equal(t, modifiedContent, comment.Content)
|
||||
|
||||
// make the comment empty
|
||||
req = NewRequestWithValues(t, "POST", fmt.Sprintf("/%s/%s/comments/%d", "user2", "repo1", commentID), map[string]string{
|
||||
"_csrf": GetCSRF(t, session, issueURL),
|
||||
"content": "",
|
||||
})
|
||||
session.MakeRequest(t, req, http.StatusOK)
|
||||
|
||||
comment = unittest.AssertExistsAndLoadBean(t, &issues_model.Comment{ID: commentID})
|
||||
assert.Equal(t, "", comment.Content)
|
||||
}
|
||||
|
||||
func TestIssueReaction(t *testing.T) {
|
||||
|
|
|
@ -80,4 +80,26 @@ func TestLFSRender(t *testing.T) {
|
|||
content := doc.Find("div.file-view").Text()
|
||||
assert.Contains(t, content, "Testing READMEs in LFS")
|
||||
})
|
||||
|
||||
t.Run("/settings/lfs/pointers", func(t *testing.T) {
|
||||
defer tests.PrintCurrentTest(t)()
|
||||
|
||||
// visit /user2/lfs/settings/lfs/pointer
|
||||
req := NewRequest(t, "GET", "/user2/lfs/settings/lfs/pointers")
|
||||
resp := session.MakeRequest(t, req, http.StatusOK)
|
||||
|
||||
// follow the first link to /user2/lfs/settings/lfs/find?oid=....
|
||||
filesTable := NewHTMLParser(t, resp.Body).doc.Find("#lfs-files-table")
|
||||
assert.Contains(t, filesTable.Text(), "Find commits")
|
||||
lfsFind := filesTable.Find(`.primary.button[href^="/user2"]`)
|
||||
assert.Greater(t, lfsFind.Length(), 0)
|
||||
lfsFindPath, exists := lfsFind.First().Attr("href")
|
||||
assert.True(t, exists)
|
||||
|
||||
assert.Contains(t, lfsFindPath, "oid=")
|
||||
req = NewRequest(t, "GET", lfsFindPath)
|
||||
resp = session.MakeRequest(t, req, http.StatusOK)
|
||||
doc := NewHTMLParser(t, resp.Body).doc
|
||||
assert.Equal(t, 1, doc.Find(`.sha.label[href="/user2/lfs/commit/73cf03db6ece34e12bf91e8853dc58f678f2f82d"]`).Length(), "could not find link to commit")
|
||||
})
|
||||
}
|
||||
|
|
205
tests/integration/remote_test.go
Normal file
205
tests/integration/remote_test.go
Normal file
|
@ -0,0 +1,205 @@
|
|||
// Copyright Earl Warren <contact@earl-warren.org>
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package integration
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"testing"
|
||||
|
||||
auth_model "code.gitea.io/gitea/models/auth"
|
||||
"code.gitea.io/gitea/models/unittest"
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
"code.gitea.io/gitea/modules/test"
|
||||
remote_service "code.gitea.io/gitea/services/remote"
|
||||
"code.gitea.io/gitea/tests"
|
||||
|
||||
"github.com/markbates/goth"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestRemote_MaybePromoteUserSuccess(t *testing.T) {
|
||||
defer tests.PrepareTestEnv(t)()
|
||||
|
||||
//
|
||||
// OAuth2 authentication source GitLab
|
||||
//
|
||||
gitlabName := "gitlab"
|
||||
_ = addAuthSource(t, authSourcePayloadGitLabCustom(gitlabName))
|
||||
//
|
||||
// Remote authentication source matching the GitLab authentication source
|
||||
//
|
||||
remoteName := "remote"
|
||||
remote := createRemoteAuthSource(t, remoteName, "http://mygitlab.eu", gitlabName)
|
||||
|
||||
//
|
||||
// Create a user as if it had previously been created by the remote
|
||||
// authentication source.
|
||||
//
|
||||
gitlabUserID := "5678"
|
||||
gitlabEmail := "gitlabuser@example.com"
|
||||
userBeforeSignIn := &user_model.User{
|
||||
Name: "gitlabuser",
|
||||
Type: user_model.UserTypeRemoteUser,
|
||||
LoginType: auth_model.Remote,
|
||||
LoginSource: remote.ID,
|
||||
LoginName: gitlabUserID,
|
||||
}
|
||||
defer createUser(context.Background(), t, userBeforeSignIn)()
|
||||
|
||||
//
|
||||
// A request for user information sent to Goth will return a
|
||||
// goth.User exactly matching the user created above.
|
||||
//
|
||||
defer mockCompleteUserAuth(func(res http.ResponseWriter, req *http.Request) (goth.User, error) {
|
||||
return goth.User{
|
||||
Provider: gitlabName,
|
||||
UserID: gitlabUserID,
|
||||
Email: gitlabEmail,
|
||||
}, nil
|
||||
})()
|
||||
req := NewRequest(t, "GET", fmt.Sprintf("/user/oauth2/%s/callback?code=XYZ&state=XYZ", gitlabName))
|
||||
resp := MakeRequest(t, req, http.StatusSeeOther)
|
||||
assert.Equal(t, "/", test.RedirectURL(resp))
|
||||
userAfterSignIn := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: userBeforeSignIn.ID})
|
||||
|
||||
// both are about the same user
|
||||
assert.Equal(t, userAfterSignIn.ID, userBeforeSignIn.ID)
|
||||
// the login time was updated, proof the login succeeded
|
||||
assert.Greater(t, userAfterSignIn.LastLoginUnix, userBeforeSignIn.LastLoginUnix)
|
||||
// the login type was promoted from Remote to OAuth2
|
||||
assert.Equal(t, userBeforeSignIn.LoginType, auth_model.Remote)
|
||||
assert.Equal(t, userAfterSignIn.LoginType, auth_model.OAuth2)
|
||||
// the OAuth2 email was used to set the missing user email
|
||||
assert.Equal(t, userBeforeSignIn.Email, "")
|
||||
assert.Equal(t, userAfterSignIn.Email, gitlabEmail)
|
||||
}
|
||||
|
||||
func TestRemote_MaybePromoteUserFail(t *testing.T) {
|
||||
defer tests.PrepareTestEnv(t)()
|
||||
|
||||
ctx := context.Background()
|
||||
//
|
||||
// OAuth2 authentication source GitLab
|
||||
//
|
||||
gitlabName := "gitlab"
|
||||
gitlabSource := addAuthSource(t, authSourcePayloadGitLabCustom(gitlabName))
|
||||
//
|
||||
// Remote authentication source matching the GitLab authentication source
|
||||
//
|
||||
remoteName := "remote"
|
||||
remoteSource := createRemoteAuthSource(t, remoteName, "http://mygitlab.eu", gitlabName)
|
||||
|
||||
{
|
||||
promoted, reason, err := remote_service.MaybePromoteRemoteUser(ctx, &auth_model.Source{}, "", "")
|
||||
assert.NoError(t, err)
|
||||
assert.False(t, promoted)
|
||||
assert.Equal(t, remote_service.ReasonNotAuth2, reason)
|
||||
}
|
||||
|
||||
{
|
||||
remoteSource.Type = auth_model.OAuth2
|
||||
promoted, reason, err := remote_service.MaybePromoteRemoteUser(ctx, remoteSource, "", "")
|
||||
assert.NoError(t, err)
|
||||
assert.False(t, promoted)
|
||||
assert.Equal(t, remote_service.ReasonBadAuth2, reason)
|
||||
remoteSource.Type = auth_model.Remote
|
||||
}
|
||||
|
||||
{
|
||||
promoted, reason, err := remote_service.MaybePromoteRemoteUser(ctx, gitlabSource, "unknownloginname", "")
|
||||
assert.NoError(t, err)
|
||||
assert.False(t, promoted)
|
||||
assert.Equal(t, remote_service.ReasonLoginNameNotExists, reason)
|
||||
}
|
||||
|
||||
{
|
||||
remoteUserID := "844"
|
||||
remoteUser := &user_model.User{
|
||||
Name: "withmailuser",
|
||||
Type: user_model.UserTypeRemoteUser,
|
||||
LoginType: auth_model.Remote,
|
||||
LoginSource: remoteSource.ID,
|
||||
LoginName: remoteUserID,
|
||||
Email: "some@example.com",
|
||||
}
|
||||
defer createUser(context.Background(), t, remoteUser)()
|
||||
promoted, reason, err := remote_service.MaybePromoteRemoteUser(ctx, gitlabSource, remoteUserID, "")
|
||||
assert.NoError(t, err)
|
||||
assert.False(t, promoted)
|
||||
assert.Equal(t, remote_service.ReasonEmailIsSet, reason)
|
||||
}
|
||||
|
||||
{
|
||||
remoteUserID := "7464"
|
||||
nonexistentloginsource := int64(4344)
|
||||
remoteUser := &user_model.User{
|
||||
Name: "badsourceuser",
|
||||
Type: user_model.UserTypeRemoteUser,
|
||||
LoginType: auth_model.Remote,
|
||||
LoginSource: nonexistentloginsource,
|
||||
LoginName: remoteUserID,
|
||||
}
|
||||
defer createUser(context.Background(), t, remoteUser)()
|
||||
promoted, reason, err := remote_service.MaybePromoteRemoteUser(ctx, gitlabSource, remoteUserID, "")
|
||||
assert.NoError(t, err)
|
||||
assert.False(t, promoted)
|
||||
assert.Equal(t, remote_service.ReasonNoSource, reason)
|
||||
}
|
||||
|
||||
{
|
||||
remoteUserID := "33335678"
|
||||
remoteUser := &user_model.User{
|
||||
Name: "badremoteuser",
|
||||
Type: user_model.UserTypeRemoteUser,
|
||||
LoginType: auth_model.Remote,
|
||||
LoginSource: gitlabSource.ID,
|
||||
LoginName: remoteUserID,
|
||||
}
|
||||
defer createUser(context.Background(), t, remoteUser)()
|
||||
promoted, reason, err := remote_service.MaybePromoteRemoteUser(ctx, gitlabSource, remoteUserID, "")
|
||||
assert.NoError(t, err)
|
||||
assert.False(t, promoted)
|
||||
assert.Equal(t, remote_service.ReasonSourceWrongType, reason)
|
||||
}
|
||||
|
||||
{
|
||||
unrelatedName := "unrelated"
|
||||
unrelatedSource := addAuthSource(t, authSourcePayloadGitHubCustom(unrelatedName))
|
||||
assert.NotNil(t, unrelatedSource)
|
||||
|
||||
remoteUserID := "488484"
|
||||
remoteEmail := "4848484@example.com"
|
||||
remoteUser := &user_model.User{
|
||||
Name: "unrelateduser",
|
||||
Type: user_model.UserTypeRemoteUser,
|
||||
LoginType: auth_model.Remote,
|
||||
LoginSource: remoteSource.ID,
|
||||
LoginName: remoteUserID,
|
||||
}
|
||||
defer createUser(context.Background(), t, remoteUser)()
|
||||
promoted, reason, err := remote_service.MaybePromoteRemoteUser(ctx, unrelatedSource, remoteUserID, remoteEmail)
|
||||
assert.NoError(t, err)
|
||||
assert.False(t, promoted)
|
||||
assert.Equal(t, remote_service.ReasonNoMatch, reason)
|
||||
}
|
||||
|
||||
{
|
||||
remoteUserID := "5678"
|
||||
remoteEmail := "gitlabuser@example.com"
|
||||
remoteUser := &user_model.User{
|
||||
Name: "remoteuser",
|
||||
Type: user_model.UserTypeRemoteUser,
|
||||
LoginType: auth_model.Remote,
|
||||
LoginSource: remoteSource.ID,
|
||||
LoginName: remoteUserID,
|
||||
}
|
||||
defer createUser(context.Background(), t, remoteUser)()
|
||||
promoted, reason, err := remote_service.MaybePromoteRemoteUser(ctx, gitlabSource, remoteUserID, remoteEmail)
|
||||
assert.NoError(t, err)
|
||||
assert.True(t, promoted)
|
||||
assert.Equal(t, remote_service.ReasonPromoted, reason)
|
||||
}
|
||||
}
|
|
@ -402,7 +402,7 @@ td .commit-summary {
|
|||
|
||||
.pdf-content {
|
||||
width: 100%;
|
||||
height: 600px;
|
||||
height: 100vh;
|
||||
border: none !important;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
|
Loading…
Reference in a new issue