mirror of
https://github.com/go-gitea/gitea
synced 2026-02-06 16:10:56 +00:00
Compare commits
35 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
dcce96c08d | ||
|
|
885f2b89d6 | ||
|
|
57ce10c0ca | ||
|
|
25785041e7 | ||
|
|
ff3d11034d | ||
|
|
750649c1ef | ||
|
|
eb95bbc1fd | ||
|
|
369830bada | ||
|
|
d7d6533311 | ||
|
|
c326369f47 | ||
|
|
4cdb8a7f96 | ||
|
|
38125a8d1d | ||
|
|
175a425825 | ||
|
|
6132f639e7 | ||
|
|
dfe4055b92 | ||
|
|
5fe9703586 | ||
|
|
53d67dae28 | ||
|
|
ef6ab681f7 | ||
|
|
812a3cffb3 | ||
|
|
669b22100b | ||
|
|
a0c77673ff | ||
|
|
d96b68cbf5 | ||
|
|
f8ec5b3e43 | ||
|
|
11891c2dac | ||
|
|
2c778ff067 | ||
|
|
83ce45b186 | ||
|
|
39e83bd3fd | ||
|
|
c2f9edd673 | ||
|
|
aa575672ac | ||
|
|
e9c14723b6 | ||
|
|
76b6e94b5b | ||
|
|
163113d173 | ||
|
|
7d010c6932 | ||
|
|
b71e688634 | ||
|
|
e147a8223a |
@@ -4,6 +4,38 @@ This changelog goes through the changes that have been made in each release
|
||||
without substantial changes to our git log; to see the highlights of what has
|
||||
been added to each release, please refer to the [blog](https://blog.gitea.com).
|
||||
|
||||
## [1.25.4](https://github.com/go-gitea/gitea/releases/tag/1.25.4) - 2026-01-15
|
||||
|
||||
* SECURITY
|
||||
* Release attachments must belong to the intended repo (#36347) (#36375)
|
||||
* Fix permission check on org project operations (#36318) (#36373)
|
||||
* Clean watches when make a repository private and check permission when send release emails (#36319) (#36370)
|
||||
* Add more check for stopwatch read or list (#36340) (#36368)
|
||||
* Fix openid setting check (#36346) (#36361)
|
||||
* Fix cancel auto merge bug (#36341) (#36356)
|
||||
* Fix delete attachment check (#36320) (#36355)
|
||||
* LFS locks must belong to the intended repo (#36344) (#36349)
|
||||
* Fix bug on notification read (#36339) #36387
|
||||
* ENHANCEMENTS
|
||||
* Add more routes to the "expensive" list (#36290)
|
||||
* Make "commit statuses" API accept slashes in "ref" (#36264) (#36275)
|
||||
* BUGFIXES
|
||||
* Fix git http service handling #36396
|
||||
* Fix markdown newline handling during IME composition (#36421) #36424
|
||||
* Fix missing repository id when migrating release attachments (#36389)
|
||||
* Fix bug when compare in the pull request (#36363) (#36372)
|
||||
* Fix incorrect text content detection (#36364) (#36369)
|
||||
* Fill missing `has_code` in repository api (#36338) (#36359)
|
||||
* Fix notifications pagination query parameters (#36351) (#36358)
|
||||
* Fix some trivial problems (#36336) (#36337)
|
||||
* Prevent panic when GitLab release has more links than sources (#36295) (#36305)
|
||||
* Fix stats bug when syncing release (#36285) (#36294)
|
||||
* Always honor user's choice for "delete branch after merge" (#36281) (#36286)
|
||||
* Use the requested host for LFS links (#36242) (#36258)
|
||||
* Fix panic when get editor config file (#36241) (#36247)
|
||||
* Fix regression in writing authorized principals (#36213) (#36218)
|
||||
* Fix WebAuthn error checking (#36219) (#36235)
|
||||
|
||||
## [1.25.3](https://github.com/go-gitea/gitea/releases/tag/1.25.3) - 2025-12-17
|
||||
|
||||
* SECURITY
|
||||
|
||||
+13
-11
@@ -163,6 +163,14 @@ func (n *nilWriter) WriteString(s string) (int, error) {
|
||||
return len(s), nil
|
||||
}
|
||||
|
||||
func parseGitHookCommitRefLine(line string) (oldCommitID, newCommitID string, refFullName git.RefName, ok bool) {
|
||||
fields := strings.Split(line, " ")
|
||||
if len(fields) != 3 {
|
||||
return "", "", "", false
|
||||
}
|
||||
return fields[0], fields[1], git.RefName(fields[2]), true
|
||||
}
|
||||
|
||||
func runHookPreReceive(ctx context.Context, c *cli.Command) error {
|
||||
if isInternal, _ := strconv.ParseBool(os.Getenv(repo_module.EnvIsInternal)); isInternal {
|
||||
return nil
|
||||
@@ -228,14 +236,11 @@ Gitea or set your environment appropriately.`, "")
|
||||
continue
|
||||
}
|
||||
|
||||
fields := bytes.Fields(scanner.Bytes())
|
||||
if len(fields) != 3 {
|
||||
oldCommitID, newCommitID, refFullName, ok := parseGitHookCommitRefLine(scanner.Text())
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
oldCommitID := string(fields[0])
|
||||
newCommitID := string(fields[1])
|
||||
refFullName := git.RefName(fields[2])
|
||||
total++
|
||||
lastline++
|
||||
|
||||
@@ -378,16 +383,13 @@ Gitea or set your environment appropriately.`, "")
|
||||
continue
|
||||
}
|
||||
|
||||
fields := bytes.Fields(scanner.Bytes())
|
||||
if len(fields) != 3 {
|
||||
var ok bool
|
||||
oldCommitIDs[count], newCommitIDs[count], refFullNames[count], ok = parseGitHookCommitRefLine(scanner.Text())
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
fmt.Fprintf(out, ".")
|
||||
oldCommitIDs[count] = string(fields[0])
|
||||
newCommitIDs[count] = string(fields[1])
|
||||
refFullNames[count] = git.RefName(fields[2])
|
||||
|
||||
commitID, _ := git.NewIDFromString(newCommitIDs[count])
|
||||
if refFullNames[count] == git.BranchPrefix+"master" && !commitID.IsZero() && count == total {
|
||||
masterPushed = true
|
||||
|
||||
@@ -39,3 +39,17 @@ func TestPktLine(t *testing.T) {
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, []byte("0007a\nb"), w.Bytes())
|
||||
}
|
||||
|
||||
func TestParseGitHookCommitRefLine(t *testing.T) {
|
||||
oldCommitID, newCommitID, refName, ok := parseGitHookCommitRefLine("a b c")
|
||||
assert.True(t, ok)
|
||||
assert.Equal(t, "a", oldCommitID)
|
||||
assert.Equal(t, "b", newCommitID)
|
||||
assert.Equal(t, "c", string(refName))
|
||||
|
||||
_, _, _, ok = parseGitHookCommitRefLine("a\tb\tc")
|
||||
assert.False(t, ok)
|
||||
|
||||
_, _, _, ok = parseGitHookCommitRefLine("a b")
|
||||
assert.False(t, ok)
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ module code.gitea.io/gitea
|
||||
|
||||
go 1.25.0
|
||||
|
||||
toolchain go1.25.5
|
||||
toolchain go1.25.7
|
||||
|
||||
// rfc5280 said: "The serial number is an integer assigned by the CA to each certificate."
|
||||
// But some CAs use negative serial number, just relax the check. related:
|
||||
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
@@ -50,12 +51,42 @@ func WriteAuthorizedStringForValidKey(key *PublicKey, w io.Writer) error {
|
||||
return err
|
||||
}
|
||||
|
||||
var globalVars = sync.OnceValue(func() (ret struct {
|
||||
principalRegexp *regexp.Regexp
|
||||
},
|
||||
) {
|
||||
// principalRegexp expresses whether a principal is considered valid.
|
||||
// This reverse engineers how sshd parses the authorized keys file,
|
||||
// see e.g. https://github.com/openssh/openssh-portable/blob/32deb00b38b4ee2b3302f261ea1e68c04e020a08/auth2-pubkeyfile.c#L221-L256
|
||||
// Any newline or # comment will be stripped when parsing, so don't allow
|
||||
// those. Also, if any space or tab is present in the principal, the part
|
||||
// proceeding this would be parsed as an option, so just avoid any whitespace
|
||||
// altogether.
|
||||
ret.principalRegexp = regexp.MustCompile(`^[^\s#]+$`)
|
||||
return ret
|
||||
})
|
||||
|
||||
func writeAuthorizedStringForKey(key *PublicKey, w io.Writer) (keyValid bool, err error) {
|
||||
const tpl = AuthorizedStringCommentPrefix + "\n" + `command=%s,no-port-forwarding,no-X11-forwarding,no-agent-forwarding,no-pty,no-user-rc,restrict %s %s` + "\n"
|
||||
pubKey, _, _, _, err := ssh.ParseAuthorizedKey([]byte(key.Content))
|
||||
if err != nil {
|
||||
return false, err
|
||||
const tpl = AuthorizedStringCommentPrefix + "\n" + `command=%s,no-port-forwarding,no-X11-forwarding,no-agent-forwarding,no-pty,no-user-rc,restrict %s` + "\n"
|
||||
|
||||
var sshKey string
|
||||
|
||||
if key.Type == KeyTypePrincipal {
|
||||
// TODO: actually using PublicKey to store "principal" is an abuse
|
||||
if !globalVars().principalRegexp.MatchString(key.Content) {
|
||||
return false, fmt.Errorf("invalid principal key: %s", key.Content)
|
||||
}
|
||||
sshKey = fmt.Sprintf("%s # user-%d", key.Content, key.OwnerID)
|
||||
} else {
|
||||
pubKey, _, _, _, err := ssh.ParseAuthorizedKey([]byte(key.Content))
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
sshKeyMarshalled := strings.TrimSpace(string(ssh.MarshalAuthorizedKey(pubKey)))
|
||||
sshKey = fmt.Sprintf("%s user-%d", sshKeyMarshalled, key.OwnerID)
|
||||
}
|
||||
|
||||
// now the key is valid, the code below could only return template/IO related errors
|
||||
sbCmd := &strings.Builder{}
|
||||
err = setting.SSH.AuthorizedKeysCommandTemplateTemplate.Execute(sbCmd, map[string]any{
|
||||
@@ -69,9 +100,7 @@ func writeAuthorizedStringForKey(key *PublicKey, w io.Writer) (keyValid bool, er
|
||||
return true, err
|
||||
}
|
||||
sshCommandEscaped := util.ShellEscape(sbCmd.String())
|
||||
sshKeyMarshalled := strings.TrimSpace(string(ssh.MarshalAuthorizedKey(pubKey)))
|
||||
sshKeyComment := fmt.Sprintf("user-%d", key.OwnerID)
|
||||
_, err = fmt.Fprintf(w, tpl, sshCommandEscaped, sshKeyMarshalled, sshKeyComment)
|
||||
_, err = fmt.Fprintf(w, tpl, sshCommandEscaped, sshKey)
|
||||
return true, err
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,90 @@
|
||||
// Copyright 2025 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package asymkey
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/modules/test"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestWriteAuthorizedStringForKey(t *testing.T) {
|
||||
defer test.MockVariableValue(&setting.AppPath, "/tmp/gitea")()
|
||||
defer test.MockVariableValue(&setting.CustomConf, "/tmp/app.ini")()
|
||||
writeKey := func(t *testing.T, key *PublicKey) (bool, string, error) {
|
||||
sb := &strings.Builder{}
|
||||
valid, err := writeAuthorizedStringForKey(key, sb)
|
||||
return valid, sb.String(), err
|
||||
}
|
||||
const validKeyContent = `ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAICV0MGX/W9IvLA4FXpIuUcdDcbj5KX4syHgsTy7soVgf`
|
||||
|
||||
testValid := func(t *testing.T, key *PublicKey, expected string) {
|
||||
valid, content, err := writeKey(t, key)
|
||||
assert.True(t, valid)
|
||||
assert.Equal(t, expected, content)
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
testInvalid := func(t *testing.T, key *PublicKey) {
|
||||
valid, content, err := writeKey(t, key)
|
||||
assert.False(t, valid)
|
||||
assert.Empty(t, content)
|
||||
assert.Error(t, err)
|
||||
}
|
||||
|
||||
t.Run("PublicKey", func(t *testing.T) {
|
||||
testValid(t, &PublicKey{
|
||||
OwnerID: 123,
|
||||
Content: validKeyContent + " any-comment",
|
||||
Type: KeyTypeUser,
|
||||
}, `# gitea public key
|
||||
command="/tmp/gitea --config=/tmp/app.ini serv key-0",no-port-forwarding,no-X11-forwarding,no-agent-forwarding,no-pty,no-user-rc,restrict ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAICV0MGX/W9IvLA4FXpIuUcdDcbj5KX4syHgsTy7soVgf user-123
|
||||
`)
|
||||
})
|
||||
|
||||
t.Run("PublicKeyWithNewLine", func(t *testing.T) {
|
||||
testValid(t, &PublicKey{
|
||||
OwnerID: 123,
|
||||
Content: validKeyContent + "\nany-more", // the new line should be ignored
|
||||
Type: KeyTypeUser,
|
||||
}, `# gitea public key
|
||||
command="/tmp/gitea --config=/tmp/app.ini serv key-0",no-port-forwarding,no-X11-forwarding,no-agent-forwarding,no-pty,no-user-rc,restrict ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAICV0MGX/W9IvLA4FXpIuUcdDcbj5KX4syHgsTy7soVgf user-123
|
||||
`)
|
||||
})
|
||||
|
||||
t.Run("PublicKeyInvalid", func(t *testing.T) {
|
||||
testInvalid(t, &PublicKey{
|
||||
OwnerID: 123,
|
||||
Content: validKeyContent + "any-more",
|
||||
Type: KeyTypeUser,
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("Principal", func(t *testing.T) {
|
||||
testValid(t, &PublicKey{
|
||||
OwnerID: 123,
|
||||
Content: "any-content",
|
||||
Type: KeyTypePrincipal,
|
||||
}, `# gitea public key
|
||||
command="/tmp/gitea --config=/tmp/app.ini serv key-0",no-port-forwarding,no-X11-forwarding,no-agent-forwarding,no-pty,no-user-rc,restrict any-content # user-123
|
||||
`)
|
||||
})
|
||||
|
||||
t.Run("PrincipalInvalid", func(t *testing.T) {
|
||||
testInvalid(t, &PublicKey{
|
||||
OwnerID: 123,
|
||||
Content: "a b",
|
||||
Type: KeyTypePrincipal,
|
||||
})
|
||||
testInvalid(t, &PublicKey{
|
||||
OwnerID: 123,
|
||||
Content: "a\nb",
|
||||
Type: KeyTypePrincipal,
|
||||
})
|
||||
})
|
||||
}
|
||||
@@ -108,10 +108,10 @@ func GetLFSLock(ctx context.Context, repo *repo_model.Repository, path string) (
|
||||
return rel, nil
|
||||
}
|
||||
|
||||
// GetLFSLockByID returns release by given id.
|
||||
func GetLFSLockByID(ctx context.Context, id int64) (*LFSLock, error) {
|
||||
// GetLFSLockByIDAndRepo returns lfs lock by given id and repository id.
|
||||
func GetLFSLockByIDAndRepo(ctx context.Context, id, repoID int64) (*LFSLock, error) {
|
||||
lock := new(LFSLock)
|
||||
has, err := db.GetEngine(ctx).ID(id).Get(lock)
|
||||
has, err := db.GetEngine(ctx).ID(id).And("repo_id = ?", repoID).Get(lock)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
} else if !has {
|
||||
@@ -160,7 +160,7 @@ func CountLFSLockByRepoID(ctx context.Context, repoID int64) (int64, error) {
|
||||
// DeleteLFSLockByID deletes a lock by given ID.
|
||||
func DeleteLFSLockByID(ctx context.Context, id int64, repo *repo_model.Repository, u *user_model.User, force bool) (*LFSLock, error) {
|
||||
return db.WithTx2(ctx, func(ctx context.Context) (*LFSLock, error) {
|
||||
lock, err := GetLFSLockByID(ctx, id)
|
||||
lock, err := GetLFSLockByIDAndRepo(ctx, id, repo.ID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package git
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
repo_model "code.gitea.io/gitea/models/repo"
|
||||
"code.gitea.io/gitea/models/unittest"
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func createTestLock(t *testing.T, repo *repo_model.Repository, owner *user_model.User) *LFSLock {
|
||||
t.Helper()
|
||||
|
||||
path := fmt.Sprintf("%s-%d-%d", t.Name(), repo.ID, time.Now().UnixNano())
|
||||
lock, err := CreateLFSLock(t.Context(), repo, &LFSLock{
|
||||
OwnerID: owner.ID,
|
||||
Path: path,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
return lock
|
||||
}
|
||||
|
||||
func TestGetLFSLockByIDAndRepo(t *testing.T) {
|
||||
require.NoError(t, unittest.PrepareTestDatabase())
|
||||
|
||||
repo1 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
|
||||
repo3 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 3})
|
||||
user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
|
||||
user4 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 4})
|
||||
|
||||
lockRepo1 := createTestLock(t, repo1, user2)
|
||||
lockRepo3 := createTestLock(t, repo3, user4)
|
||||
|
||||
fetched, err := GetLFSLockByIDAndRepo(t.Context(), lockRepo1.ID, repo1.ID)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, lockRepo1.ID, fetched.ID)
|
||||
assert.Equal(t, repo1.ID, fetched.RepoID)
|
||||
|
||||
_, err = GetLFSLockByIDAndRepo(t.Context(), lockRepo1.ID, repo3.ID)
|
||||
assert.Error(t, err)
|
||||
assert.True(t, IsErrLFSLockNotExist(err))
|
||||
|
||||
_, err = GetLFSLockByIDAndRepo(t.Context(), lockRepo3.ID, repo1.ID)
|
||||
assert.Error(t, err)
|
||||
assert.True(t, IsErrLFSLockNotExist(err))
|
||||
}
|
||||
|
||||
func TestDeleteLFSLockByIDRequiresRepoMatch(t *testing.T) {
|
||||
require.NoError(t, unittest.PrepareTestDatabase())
|
||||
|
||||
repo1 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
|
||||
repo3 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 3})
|
||||
user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
|
||||
user4 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 4})
|
||||
|
||||
lockRepo1 := createTestLock(t, repo1, user2)
|
||||
lockRepo3 := createTestLock(t, repo3, user4)
|
||||
|
||||
_, err := DeleteLFSLockByID(t.Context(), lockRepo3.ID, repo1, user2, true)
|
||||
assert.Error(t, err)
|
||||
assert.True(t, IsErrLFSLockNotExist(err))
|
||||
|
||||
existing, err := GetLFSLockByIDAndRepo(t.Context(), lockRepo3.ID, repo3.ID)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, lockRepo3.ID, existing.ID)
|
||||
|
||||
deleted, err := DeleteLFSLockByID(t.Context(), lockRepo3.ID, repo3, user4, true)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, lockRepo3.ID, deleted.ID)
|
||||
|
||||
deleted, err = DeleteLFSLockByID(t.Context(), lockRepo1.ID, repo1, user2, false)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, lockRepo1.ID, deleted.ID)
|
||||
}
|
||||
@@ -12,6 +12,8 @@ import (
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
"code.gitea.io/gitea/modules/timeutil"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
|
||||
"xorm.io/builder"
|
||||
)
|
||||
|
||||
// Stopwatch represents a stopwatch for time tracking.
|
||||
@@ -232,3 +234,14 @@ func CancelStopwatch(ctx context.Context, user *user_model.User, issue *Issue) (
|
||||
})
|
||||
return ok, err
|
||||
}
|
||||
|
||||
// RemoveStopwatchesByRepoID removes all stopwatches for a user in a specific repository
|
||||
// this function should be called before removing all the issues of the repository
|
||||
func RemoveStopwatchesByRepoID(ctx context.Context, userID, repoID int64) error {
|
||||
_, err := db.GetEngine(ctx).
|
||||
Where("`stopwatch`.user_id = ?", userID).
|
||||
And(builder.In("`stopwatch`.issue_id",
|
||||
builder.Select("id").From("issue").Where(builder.Eq{"repo_id": repoID}))).
|
||||
Delete(new(Stopwatch))
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -43,13 +43,15 @@ func GetOrInsertBlob(ctx context.Context, pb *PackageBlob) (*PackageBlob, bool,
|
||||
|
||||
existing := &PackageBlob{}
|
||||
|
||||
has, err := e.Where(builder.Eq{
|
||||
hashCond := builder.Eq{
|
||||
"size": pb.Size,
|
||||
"hash_md5": pb.HashMD5,
|
||||
"hash_sha1": pb.HashSHA1,
|
||||
"hash_sha256": pb.HashSHA256,
|
||||
"hash_sha512": pb.HashSHA512,
|
||||
}).Get(existing)
|
||||
}
|
||||
|
||||
has, err := e.Where(hashCond).Get(existing)
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
@@ -57,6 +59,11 @@ func GetOrInsertBlob(ctx context.Context, pb *PackageBlob) (*PackageBlob, bool,
|
||||
return existing, true, nil
|
||||
}
|
||||
if _, err = e.Insert(pb); err != nil {
|
||||
// Handle race condition: another request may have inserted the same blob
|
||||
// between our SELECT and INSERT. Retry the SELECT to get the existing blob.
|
||||
if has, _ = e.Where(hashCond).Get(existing); has {
|
||||
return existing, true, nil
|
||||
}
|
||||
return nil, false, err
|
||||
}
|
||||
return pb, false, nil
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package packages
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"code.gitea.io/gitea/models/unittest"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"golang.org/x/sync/errgroup"
|
||||
)
|
||||
|
||||
func TestGetOrInsertBlobConcurrent(t *testing.T) {
|
||||
require.NoError(t, unittest.PrepareTestDatabase())
|
||||
|
||||
testBlob := PackageBlob{
|
||||
Size: 123,
|
||||
HashMD5: "md5",
|
||||
HashSHA1: "sha1",
|
||||
HashSHA256: "sha256",
|
||||
HashSHA512: "sha512",
|
||||
}
|
||||
|
||||
const numGoroutines = 3
|
||||
var wg errgroup.Group
|
||||
results := make([]*PackageBlob, numGoroutines)
|
||||
existed := make([]bool, numGoroutines)
|
||||
for idx := range numGoroutines {
|
||||
wg.Go(func() error {
|
||||
blob := testBlob // Create a copy of the test blob for each goroutine
|
||||
var err error
|
||||
results[idx], existed[idx], err = GetOrInsertBlob(t.Context(), &blob)
|
||||
return err
|
||||
})
|
||||
}
|
||||
require.NoError(t, wg.Wait())
|
||||
|
||||
// then: all GetOrInsertBlob succeeds with the same blob ID, and only one indicates it did not exist before
|
||||
existedCount := 0
|
||||
assert.NotNil(t, results[0])
|
||||
for i := range numGoroutines {
|
||||
assert.Equal(t, results[0].ID, results[i].ID)
|
||||
if existed[i] {
|
||||
existedCount++
|
||||
}
|
||||
}
|
||||
assert.Equal(t, numGoroutines-1, existedCount)
|
||||
}
|
||||
@@ -213,6 +213,18 @@ func GetColumn(ctx context.Context, columnID int64) (*Column, error) {
|
||||
return column, nil
|
||||
}
|
||||
|
||||
func GetColumnByIDAndProjectID(ctx context.Context, columnID, projectID int64) (*Column, error) {
|
||||
column := new(Column)
|
||||
has, err := db.GetEngine(ctx).ID(columnID).And("project_id=?", projectID).Get(column)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
} else if !has {
|
||||
return nil, ErrProjectColumnNotExist{ColumnID: columnID}
|
||||
}
|
||||
|
||||
return column, nil
|
||||
}
|
||||
|
||||
// UpdateColumn updates a project column
|
||||
func UpdateColumn(ctx context.Context, column *Column) error {
|
||||
var fieldToUpdate []string
|
||||
|
||||
@@ -302,6 +302,18 @@ func GetProjectByID(ctx context.Context, id int64) (*Project, error) {
|
||||
return p, nil
|
||||
}
|
||||
|
||||
func GetProjectByIDAndOwner(ctx context.Context, id, ownerID int64) (*Project, error) {
|
||||
p := new(Project)
|
||||
has, err := db.GetEngine(ctx).ID(id).And("owner_id = ?", ownerID).Get(p)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
} else if !has {
|
||||
return nil, ErrProjectNotExist{ID: id}
|
||||
}
|
||||
|
||||
return p, nil
|
||||
}
|
||||
|
||||
// GetProjectForRepoByID returns the projects in a repository
|
||||
func GetProjectForRepoByID(ctx context.Context, repoID, id int64) (*Project, error) {
|
||||
p := new(Project)
|
||||
|
||||
@@ -166,6 +166,11 @@ func GetAttachmentByReleaseIDFileName(ctx context.Context, releaseID int64, file
|
||||
return attach, nil
|
||||
}
|
||||
|
||||
func GetUnlinkedAttachmentsByUserID(ctx context.Context, userID int64) ([]*Attachment, error) {
|
||||
attachments := make([]*Attachment, 0, 10)
|
||||
return attachments, db.GetEngine(ctx).Where("uploader_id = ? AND issue_id = 0 AND release_id = 0 AND comment_id = 0", userID).Find(&attachments)
|
||||
}
|
||||
|
||||
// DeleteAttachment deletes the given attachment and optionally the associated file.
|
||||
func DeleteAttachment(ctx context.Context, a *Attachment, remove bool) error {
|
||||
_, err := DeleteAttachments(ctx, []*Attachment{a}, remove)
|
||||
|
||||
@@ -101,3 +101,19 @@ func TestGetAttachmentsByUUIDs(t *testing.T) {
|
||||
assert.Equal(t, int64(1), attachList[0].IssueID)
|
||||
assert.Equal(t, int64(5), attachList[1].IssueID)
|
||||
}
|
||||
|
||||
func TestGetUnlinkedAttachmentsByUserID(t *testing.T) {
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
|
||||
attachments, err := repo_model.GetUnlinkedAttachmentsByUserID(t.Context(), 8)
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, attachments, 1)
|
||||
assert.Equal(t, int64(10), attachments[0].ID)
|
||||
assert.Zero(t, attachments[0].IssueID)
|
||||
assert.Zero(t, attachments[0].ReleaseID)
|
||||
assert.Zero(t, attachments[0].CommentID)
|
||||
|
||||
attachments, err = repo_model.GetUnlinkedAttachmentsByUserID(t.Context(), 1)
|
||||
assert.NoError(t, err)
|
||||
assert.Empty(t, attachments)
|
||||
}
|
||||
|
||||
+34
-8
@@ -93,15 +93,25 @@ func init() {
|
||||
db.RegisterModel(new(Release))
|
||||
}
|
||||
|
||||
// LoadAttributes load repo and publisher attributes for a release
|
||||
func (r *Release) LoadAttributes(ctx context.Context) error {
|
||||
var err error
|
||||
if r.Repo == nil {
|
||||
r.Repo, err = GetRepositoryByID(ctx, r.RepoID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// LegacyAttachmentMissingRepoIDCutoff marks the date when repo_id started to be written during uploads
|
||||
// (2026-01-16T00:00:00Z). Older rows might have repo_id=0 and should be tolerated once.
|
||||
const LegacyAttachmentMissingRepoIDCutoff timeutil.TimeStamp = 1768521600
|
||||
|
||||
func (r *Release) LoadRepo(ctx context.Context) (err error) {
|
||||
if r.Repo != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
r.Repo, err = GetRepositoryByID(ctx, r.RepoID)
|
||||
return err
|
||||
}
|
||||
|
||||
// LoadAttributes load repo and publisher attributes for a release
|
||||
func (r *Release) LoadAttributes(ctx context.Context) (err error) {
|
||||
if err := r.LoadRepo(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if r.Publisher == nil {
|
||||
r.Publisher, err = user_model.GetUserByID(ctx, r.PublisherID)
|
||||
if err != nil {
|
||||
@@ -168,6 +178,11 @@ func UpdateReleaseNumCommits(ctx context.Context, rel *Release) error {
|
||||
|
||||
// AddReleaseAttachments adds a release attachments
|
||||
func AddReleaseAttachments(ctx context.Context, releaseID int64, attachmentUUIDs []string) (err error) {
|
||||
rel, err := GetReleaseByID(ctx, releaseID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Check attachments
|
||||
attachments, err := GetAttachmentsByUUIDs(ctx, attachmentUUIDs)
|
||||
if err != nil {
|
||||
@@ -175,6 +190,17 @@ func AddReleaseAttachments(ctx context.Context, releaseID int64, attachmentUUIDs
|
||||
}
|
||||
|
||||
for i := range attachments {
|
||||
if attachments[i].RepoID == 0 && attachments[i].CreatedUnix < LegacyAttachmentMissingRepoIDCutoff {
|
||||
attachments[i].RepoID = rel.RepoID
|
||||
if _, err = db.GetEngine(ctx).ID(attachments[i].ID).Cols("repo_id").Update(attachments[i]); err != nil {
|
||||
return fmt.Errorf("update attachment repo_id [%d]: %w", attachments[i].ID, err)
|
||||
}
|
||||
}
|
||||
|
||||
if attachments[i].RepoID != rel.RepoID {
|
||||
return util.NewPermissionDeniedErrorf("attachment belongs to different repository")
|
||||
}
|
||||
|
||||
if attachments[i].ReleaseID != 0 {
|
||||
return util.NewPermissionDeniedErrorf("release permission denied")
|
||||
}
|
||||
|
||||
@@ -6,7 +6,9 @@ package repo
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"code.gitea.io/gitea/models/db"
|
||||
"code.gitea.io/gitea/models/unittest"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
@@ -37,3 +39,54 @@ func Test_FindTagsByCommitIDs(t *testing.T) {
|
||||
assert.Equal(t, "delete-tag", rels[1].TagName)
|
||||
assert.Equal(t, "v1.0", rels[2].TagName)
|
||||
}
|
||||
|
||||
func TestAddReleaseAttachmentsRejectsDifferentRepo(t *testing.T) {
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
|
||||
uuid := "a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a12" // attachment 2 belongs to repo 2
|
||||
err := AddReleaseAttachments(t.Context(), 1, []string{uuid})
|
||||
assert.Error(t, err)
|
||||
assert.ErrorIs(t, err, util.ErrPermissionDenied)
|
||||
|
||||
attach, err := GetAttachmentByUUID(t.Context(), uuid)
|
||||
assert.NoError(t, err)
|
||||
assert.Zero(t, attach.ReleaseID, "attachment should not be linked to release on failure")
|
||||
}
|
||||
|
||||
func TestAddReleaseAttachmentsAllowsLegacyMissingRepoID(t *testing.T) {
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
|
||||
legacyUUID := "a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a20" // attachment 10 has repo_id 0
|
||||
err := AddReleaseAttachments(t.Context(), 1, []string{legacyUUID})
|
||||
assert.NoError(t, err)
|
||||
|
||||
attach, err := GetAttachmentByUUID(t.Context(), legacyUUID)
|
||||
assert.NoError(t, err)
|
||||
assert.EqualValues(t, 1, attach.RepoID)
|
||||
assert.EqualValues(t, 1, attach.ReleaseID)
|
||||
}
|
||||
|
||||
func TestAddReleaseAttachmentsRejectsRecentZeroRepoID(t *testing.T) {
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
|
||||
recentUUID := "a0eebc99-9c0b-4ef8-bb6d-6bb9bd3800aa"
|
||||
attachment := &Attachment{
|
||||
UUID: recentUUID,
|
||||
RepoID: 0,
|
||||
IssueID: 0,
|
||||
ReleaseID: 0,
|
||||
CommentID: 0,
|
||||
Name: "recent-zero",
|
||||
CreatedUnix: LegacyAttachmentMissingRepoIDCutoff + 1,
|
||||
}
|
||||
assert.NoError(t, db.Insert(t.Context(), attachment))
|
||||
|
||||
err := AddReleaseAttachments(t.Context(), 1, []string{recentUUID})
|
||||
assert.Error(t, err)
|
||||
assert.ErrorIs(t, err, util.ErrPermissionDenied)
|
||||
|
||||
attach, err := GetAttachmentByUUID(t.Context(), recentUUID)
|
||||
assert.NoError(t, err)
|
||||
assert.Zero(t, attach.ReleaseID)
|
||||
assert.Zero(t, attach.RepoID)
|
||||
}
|
||||
|
||||
@@ -176,3 +176,13 @@ func WatchIfAuto(ctx context.Context, userID, repoID int64, isWrite bool) error
|
||||
}
|
||||
return watchRepoMode(ctx, watch, WatchModeAuto)
|
||||
}
|
||||
|
||||
// ClearRepoWatches clears all watches for a repository and from the user that watched it.
|
||||
// Used when a repository is set to private.
|
||||
func ClearRepoWatches(ctx context.Context, repoID int64) error {
|
||||
if _, err := db.Exec(ctx, "UPDATE `repository` SET num_watches = 0 WHERE id = ?", repoID); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return db.DeleteBeans(ctx, Watch{RepoID: repoID})
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ import (
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestIsWatching(t *testing.T) {
|
||||
@@ -119,3 +120,21 @@ func TestWatchIfAuto(t *testing.T) {
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, watchers, prevCount)
|
||||
}
|
||||
|
||||
func TestClearRepoWatches(t *testing.T) {
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
|
||||
const repoID int64 = 1
|
||||
watchers, err := repo_model.GetRepoWatchersIDs(t.Context(), repoID)
|
||||
require.NoError(t, err)
|
||||
require.NotEmpty(t, watchers)
|
||||
|
||||
assert.NoError(t, repo_model.ClearRepoWatches(t.Context(), repoID))
|
||||
|
||||
watchers, err = repo_model.GetRepoWatchersIDs(t.Context(), repoID)
|
||||
assert.NoError(t, err)
|
||||
assert.Empty(t, watchers)
|
||||
|
||||
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: repoID})
|
||||
assert.Zero(t, repo.NumWatches)
|
||||
}
|
||||
|
||||
@@ -102,7 +102,13 @@ func DeleteUserOpenID(ctx context.Context, openid *UserOpenID) (err error) {
|
||||
}
|
||||
|
||||
// ToggleUserOpenIDVisibility toggles visibility of an openid address of given user.
|
||||
func ToggleUserOpenIDVisibility(ctx context.Context, id int64) (err error) {
|
||||
_, err = db.GetEngine(ctx).Exec("update `user_open_id` set `show` = not `show` where `id` = ?", id)
|
||||
return err
|
||||
func ToggleUserOpenIDVisibility(ctx context.Context, id int64, user *User) error {
|
||||
affected, err := db.GetEngine(ctx).Exec("update `user_open_id` set `show` = not `show` where `id` = ? AND uid = ?", id, user.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if n, _ := affected.RowsAffected(); n != 1 {
|
||||
return util.NewNotExistErrorf("OpenID is unknown")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
|
||||
"code.gitea.io/gitea/models/unittest"
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
@@ -33,12 +34,14 @@ func TestGetUserOpenIDs(t *testing.T) {
|
||||
|
||||
func TestToggleUserOpenIDVisibility(t *testing.T) {
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
user, err := user_model.GetUserByID(t.Context(), int64(2))
|
||||
require.NoError(t, err)
|
||||
oids, err := user_model.GetUserOpenIDs(t.Context(), int64(2))
|
||||
require.NoError(t, err)
|
||||
require.Len(t, oids, 1)
|
||||
assert.True(t, oids[0].Show)
|
||||
|
||||
err = user_model.ToggleUserOpenIDVisibility(t.Context(), oids[0].ID)
|
||||
err = user_model.ToggleUserOpenIDVisibility(t.Context(), oids[0].ID, user)
|
||||
require.NoError(t, err)
|
||||
|
||||
oids, err = user_model.GetUserOpenIDs(t.Context(), int64(2))
|
||||
@@ -46,7 +49,7 @@ func TestToggleUserOpenIDVisibility(t *testing.T) {
|
||||
require.Len(t, oids, 1)
|
||||
|
||||
assert.False(t, oids[0].Show)
|
||||
err = user_model.ToggleUserOpenIDVisibility(t.Context(), oids[0].ID)
|
||||
err = user_model.ToggleUserOpenIDVisibility(t.Context(), oids[0].ID, user)
|
||||
require.NoError(t, err)
|
||||
|
||||
oids, err = user_model.GetUserOpenIDs(t.Context(), int64(2))
|
||||
@@ -55,3 +58,13 @@ func TestToggleUserOpenIDVisibility(t *testing.T) {
|
||||
assert.True(t, oids[0].Show)
|
||||
}
|
||||
}
|
||||
|
||||
func TestToggleUserOpenIDVisibilityRequiresOwnership(t *testing.T) {
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
unauthorizedUser, err := user_model.GetUserByID(t.Context(), int64(2))
|
||||
require.NoError(t, err)
|
||||
|
||||
err = user_model.ToggleUserOpenIDVisibility(t.Context(), int64(1), unauthorizedUser)
|
||||
require.Error(t, err)
|
||||
assert.ErrorIs(t, err, util.ErrNotExist)
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
|
||||
activities_model "code.gitea.io/gitea/models/activities"
|
||||
issues_model "code.gitea.io/gitea/models/issues"
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
"code.gitea.io/gitea/modules/graceful"
|
||||
"code.gitea.io/gitea/modules/json"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
@@ -91,7 +92,13 @@ loop:
|
||||
}
|
||||
|
||||
for _, userStopwatches := range usersStopwatches {
|
||||
apiSWs, err := convert.ToStopWatches(ctx, userStopwatches.StopWatches)
|
||||
u, err := user_model.GetUserByID(ctx, userStopwatches.UserID)
|
||||
if err != nil {
|
||||
log.Error("Unable to get user %d: %v", userStopwatches.UserID, err)
|
||||
continue
|
||||
}
|
||||
|
||||
apiSWs, err := convert.ToStopWatches(ctx, u, userStopwatches.StopWatches)
|
||||
if err != nil {
|
||||
if !issues_model.IsErrIssueNotExist(err) {
|
||||
log.Error("Unable to APIFormat stopwatches: %v", err)
|
||||
|
||||
@@ -9,24 +9,38 @@ package git
|
||||
import (
|
||||
"io"
|
||||
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
|
||||
"github.com/go-git/go-git/v5/plumbing"
|
||||
)
|
||||
|
||||
// Blob represents a Git object.
|
||||
type Blob struct {
|
||||
ID ObjectID
|
||||
ID ObjectID
|
||||
repo *Repository
|
||||
name string
|
||||
}
|
||||
|
||||
gogitEncodedObj plumbing.EncodedObject
|
||||
name string
|
||||
func (b *Blob) gogitEncodedObj() (plumbing.EncodedObject, error) {
|
||||
return b.repo.gogitRepo.Storer.EncodedObject(plumbing.AnyObject, plumbing.Hash(b.ID.RawValue()))
|
||||
}
|
||||
|
||||
// DataAsync gets a ReadCloser for the contents of a blob without reading it all.
|
||||
// Calling the Close function on the result will discard all unread output.
|
||||
func (b *Blob) DataAsync() (io.ReadCloser, error) {
|
||||
return b.gogitEncodedObj.Reader()
|
||||
obj, err := b.gogitEncodedObj()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return obj.Reader()
|
||||
}
|
||||
|
||||
// Size returns the uncompressed size of the blob
|
||||
func (b *Blob) Size() int64 {
|
||||
return b.gogitEncodedObj.Size()
|
||||
obj, err := b.gogitEncodedObj()
|
||||
if err != nil {
|
||||
log.Error("Error getting gogit encoded object for blob %s(%s): %v", b.name, b.ID.String(), err)
|
||||
return 0
|
||||
}
|
||||
return obj.Size()
|
||||
}
|
||||
|
||||
@@ -30,9 +30,11 @@ type Parser struct {
|
||||
func NewParser(r io.Reader, format Format) *Parser {
|
||||
scanner := bufio.NewScanner(r)
|
||||
|
||||
// default MaxScanTokenSize = 64 kiB may be too small for some references,
|
||||
// so allow the buffer to grow up to 4x if needed
|
||||
scanner.Buffer(nil, 4*bufio.MaxScanTokenSize)
|
||||
// default Scanner.MaxScanTokenSize = 64 kiB may be too small for some references,
|
||||
// so allow the buffer to be large enough in case the ref has long content (e.g.: a tag with long message)
|
||||
// as long as it doesn't exceed some reasonable limit (4 MiB here, or MAX_DISPLAY_FILE_SIZE=8MiB), it is OK
|
||||
// there are still some choices: 1. add a config option for the limit; 2. don't use scanner and write our own parser to fully handle large contents
|
||||
scanner.Buffer(nil, 4*1024*1024)
|
||||
|
||||
// in addition to the reference delimiter we specified in the --format,
|
||||
// `git for-each-ref` will always add a newline after every reference.
|
||||
|
||||
@@ -9,5 +9,11 @@ func (repo *Repository) GetBlob(idStr string) (*Blob, error) {
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return repo.getBlob(id)
|
||||
if id.IsZero() {
|
||||
return nil, ErrNotExist{id.String(), ""}
|
||||
}
|
||||
return &Blob{
|
||||
ID: id,
|
||||
repo: repo,
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -1,22 +0,0 @@
|
||||
// Copyright 2018 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
//go:build gogit
|
||||
|
||||
package git
|
||||
|
||||
import (
|
||||
"github.com/go-git/go-git/v5/plumbing"
|
||||
)
|
||||
|
||||
func (repo *Repository) getBlob(id ObjectID) (*Blob, error) {
|
||||
encodedObj, err := repo.gogitRepo.Storer.EncodedObject(plumbing.AnyObject, plumbing.Hash(id.RawValue()))
|
||||
if err != nil {
|
||||
return nil, ErrNotExist{id.String(), ""}
|
||||
}
|
||||
|
||||
return &Blob{
|
||||
ID: id,
|
||||
gogitEncodedObj: encodedObj,
|
||||
}, nil
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
// Copyright 2020 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
//go:build !gogit
|
||||
|
||||
package git
|
||||
|
||||
func (repo *Repository) getBlob(id ObjectID) (*Blob, error) {
|
||||
if id.IsZero() {
|
||||
return nil, ErrNotExist{id.String(), ""}
|
||||
}
|
||||
return &Blob{
|
||||
ID: id,
|
||||
repo: repo,
|
||||
}, nil
|
||||
}
|
||||
@@ -7,7 +7,6 @@
|
||||
package git
|
||||
|
||||
import (
|
||||
"github.com/go-git/go-git/v5/plumbing"
|
||||
"github.com/go-git/go-git/v5/plumbing/filemode"
|
||||
"github.com/go-git/go-git/v5/plumbing/object"
|
||||
)
|
||||
@@ -83,14 +82,9 @@ func (te *TreeEntry) IsExecutable() bool {
|
||||
|
||||
// Blob returns the blob object the entry
|
||||
func (te *TreeEntry) Blob() *Blob {
|
||||
encodedObj, err := te.ptree.repo.gogitRepo.Storer.EncodedObject(plumbing.AnyObject, te.gogitTreeEntry.Hash)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
return &Blob{
|
||||
ID: ParseGogitHash(te.gogitTreeEntry.Hash),
|
||||
gogitEncodedObj: encodedObj,
|
||||
name: te.Name(),
|
||||
ID: ParseGogitHash(te.gogitTreeEntry.Hash),
|
||||
repo: te.ptree.repo,
|
||||
name: te.Name(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,9 +10,12 @@ import (
|
||||
|
||||
// ReleaseAsset represents a release asset
|
||||
type ReleaseAsset struct {
|
||||
ID int64
|
||||
Name string
|
||||
ContentType *string `yaml:"content_type"`
|
||||
ID int64
|
||||
Name string
|
||||
|
||||
// There was a field "ContentType (content_type)" because Some forges can provide that for assets,
|
||||
// but we don't need it when migrating, so the field is omitted here.
|
||||
|
||||
Size *int
|
||||
DownloadCount *int `yaml:"download_count"`
|
||||
Created time.Time
|
||||
|
||||
@@ -233,7 +233,7 @@ func SyncReleasesWithTags(ctx context.Context, repo *repo_model.Repository, gitR
|
||||
return fmt.Errorf("unable to update tag %s for pull-mirror Repo[%d:%s/%s]: %w", tag.Name, repo.ID, repo.OwnerName, repo.Name, err)
|
||||
}
|
||||
}
|
||||
added, deleted, updated = len(deletes), len(updates), len(inserts)
|
||||
added, deleted, updated = len(inserts), len(deletes), len(updates)
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
|
||||
@@ -107,6 +107,17 @@ func detectFileTypeBox(data []byte) (brands []string, found bool) {
|
||||
return brands, true
|
||||
}
|
||||
|
||||
func isEmbeddedOpenType(data []byte) bool {
|
||||
// https://www.w3.org/submissions/EOT
|
||||
if len(data) < 80 {
|
||||
return false
|
||||
}
|
||||
version := binary.LittleEndian.Uint32(data[8:]) // Actually this standard is abandoned (for IE6-IE11 only), there are only 3 versions defined
|
||||
magic := binary.LittleEndian.Uint16(data[34:36]) // MagicNumber: 0x504C ("LP")
|
||||
reserved := data[64:80] // Reserved 1-4 (each: unsigned long)
|
||||
return (version == 0x00010000 || version == 0x00020001 || version == 0x00020002) && magic == 0x504C && bytes.Count(reserved, []byte{0}) == len(reserved)
|
||||
}
|
||||
|
||||
// DetectContentType extends http.DetectContentType with more content types. Defaults to text/plain if input is empty.
|
||||
func DetectContentType(data []byte) SniffedType {
|
||||
if len(data) == 0 {
|
||||
@@ -119,6 +130,18 @@ func DetectContentType(data []byte) SniffedType {
|
||||
data = data[:SniffContentSize]
|
||||
}
|
||||
|
||||
const typeMsFontObject = "application/vnd.ms-fontobject"
|
||||
if ct == typeMsFontObject {
|
||||
// Stupid Golang blindly detects any content with 34th-35th bytes being "LP" as font.
|
||||
// If it is not really for ".eot" content, we try to detect it again by hiding the "LP", see the test for more details.
|
||||
if isEmbeddedOpenType(data) {
|
||||
return SniffedType{typeMsFontObject}
|
||||
}
|
||||
data = slices.Clone(data)
|
||||
data[34] = 'l'
|
||||
ct = http.DetectContentType(data)
|
||||
}
|
||||
|
||||
vars := globalVars()
|
||||
// SVG is unsupported by http.DetectContentType, https://github.com/golang/go/issues/15888
|
||||
detectByHTML := strings.Contains(ct, "text/plain") || strings.Contains(ct, "text/html")
|
||||
|
||||
@@ -6,6 +6,7 @@ package typesniffer
|
||||
import (
|
||||
"encoding/base64"
|
||||
"encoding/hex"
|
||||
"net/http"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
@@ -154,3 +155,25 @@ func TestDetectContentTypeAvif(t *testing.T) {
|
||||
st := DetectContentType(buf)
|
||||
assert.Equal(t, MimeTypeImageAvif, st.contentType)
|
||||
}
|
||||
|
||||
func TestDetectContentTypeIncorrectFont(t *testing.T) {
|
||||
s := "Stupid Golang keep detecting 34th LP as font"
|
||||
// They don't want to have any improvement to it: https://github.com/golang/go/issues/77172
|
||||
golangDetected := http.DetectContentType([]byte(s))
|
||||
assert.Equal(t, "application/vnd.ms-fontobject", golangDetected)
|
||||
// We have to make our patch to make it work correctly
|
||||
ourDetected := DetectContentType([]byte(s))
|
||||
assert.Equal(t, "text/plain; charset=utf-8", ourDetected.contentType)
|
||||
|
||||
// For binary content, ensure it still detects as font. The content is from "opensans-regular.eot"
|
||||
b := []byte{
|
||||
0x3d, 0x30, 0x00, 0x00, 0x6b, 0x2f, 0x00, 0x00, 0x02, 0x00, 0x02, 0x00, 0x04, 0x00, 0x00, 0x00,
|
||||
0x02, 0x0b, 0x06, 0x06, 0x03, 0x05, 0x04, 0x02, 0x02, 0x04, 0x01, 0x00, 0x90, 0x01, 0x00, 0x00,
|
||||
0x04, 0x00, 0x4c, 0x50, 0xef, 0x02, 0x00, 0xe0, 0x5b, 0x20, 0x00, 0x40, 0x28, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x9f, 0x01, 0x00, 0x20, 0x00, 0x00, 0x00, 0x00, 0x63, 0xf4, 0x17, 0x14,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x12, 0x00, 0x4f, 0x00, 0x70, 0x00, 0x65, 0x00, 0x6e, 0x00, 0x20, 0x00, 0x53, 0x00,
|
||||
}
|
||||
assert.Equal(t, "application/vnd.ms-fontobject", http.DetectContentType(b))
|
||||
assert.Equal(t, "application/vnd.ms-fontobject", DetectContentType(b).contentType)
|
||||
}
|
||||
|
||||
@@ -26,9 +26,18 @@ import (
|
||||
|
||||
// saveAsPackageBlob creates a package blob from an upload
|
||||
// The uploaded blob gets stored in a special upload version to link them to the package/image
|
||||
func saveAsPackageBlob(ctx context.Context, hsr packages_module.HashedSizeReader, pci *packages_service.PackageCreationInfo) (*packages_model.PackageBlob, error) { //nolint:unparam // PackageBlob is never used
|
||||
// There will be concurrent uploading for the same blob, so it needs a global lock per blob hash
|
||||
func saveAsPackageBlob(ctx context.Context, hsr packages_module.HashedSizeReader, pci *packages_service.PackageCreationInfo) (*packages_model.PackageBlob, error) { //nolint:unparam //returned PackageBlob is never used
|
||||
pb := packages_service.NewPackageBlob(hsr)
|
||||
err := globallock.LockAndDo(ctx, "container-blob:"+pb.HashSHA256, func(ctx context.Context) error {
|
||||
var err error
|
||||
pb, err = saveAsPackageBlobInternal(ctx, hsr, pci, pb)
|
||||
return err
|
||||
})
|
||||
return pb, err
|
||||
}
|
||||
|
||||
func saveAsPackageBlobInternal(ctx context.Context, hsr packages_module.HashedSizeReader, pci *packages_service.PackageCreationInfo, pb *packages_model.PackageBlob) (*packages_model.PackageBlob, error) {
|
||||
exists := false
|
||||
|
||||
contentStore := packages_module.NewContentStore()
|
||||
@@ -67,7 +76,7 @@ func saveAsPackageBlob(ctx context.Context, hsr packages_module.HashedSizeReader
|
||||
return createFileForBlob(ctx, uploadVersion, pb)
|
||||
})
|
||||
if err != nil {
|
||||
if !exists {
|
||||
if !exists && pb != nil { // pb can be nil if GetOrInsertBlob failed
|
||||
if err := contentStore.Delete(packages_module.BlobHash256Key(pb.HashSHA256)); err != nil {
|
||||
log.Error("Error deleting package blob from content store: %v", err)
|
||||
}
|
||||
|
||||
@@ -1400,19 +1400,19 @@ func Routes() *web.Router {
|
||||
})
|
||||
m.Get("/{base}/*", repo.GetPullRequestByBaseHead)
|
||||
}, mustAllowPulls, reqRepoReader(unit.TypeCode), context.ReferencesGitRepo())
|
||||
m.Group("/statuses", func() {
|
||||
m.Group("/statuses", func() { // "/statuses/{sha}" only accepts commit ID
|
||||
m.Combo("/{sha}").Get(repo.GetCommitStatuses).
|
||||
Post(reqToken(), reqRepoWriter(unit.TypeCode), bind(api.CreateStatusOption{}), repo.NewCommitStatus)
|
||||
}, reqRepoReader(unit.TypeCode))
|
||||
m.Group("/commits", func() {
|
||||
m.Get("", context.ReferencesGitRepo(), repo.GetAllCommits)
|
||||
m.Group("/{ref}", func() {
|
||||
m.Get("/status", repo.GetCombinedCommitStatusByRef)
|
||||
m.Get("/statuses", repo.GetCommitStatusesByRef)
|
||||
}, context.ReferencesGitRepo())
|
||||
m.Group("/{sha}", func() {
|
||||
m.Get("/pull", repo.GetCommitPullRequest)
|
||||
}, context.ReferencesGitRepo())
|
||||
m.PathGroup("/*", func(g *web.RouterPathGroup) {
|
||||
// Mis-configured reverse proxy might decode the `%2F` to slash ahead, so we need to support both formats (escaped, unescaped) here.
|
||||
// It also matches GitHub's behavior
|
||||
g.MatchPath("GET", "/<ref:*>/status", repo.GetCombinedCommitStatusByRef)
|
||||
g.MatchPath("GET", "/<ref:*>/statuses", repo.GetCommitStatusesByRef)
|
||||
g.MatchPath("GET", "/<sha>/pull", repo.GetCommitPullRequest)
|
||||
})
|
||||
}, reqRepoReader(unit.TypeCode))
|
||||
m.Group("/git", func() {
|
||||
m.Group("/commits", func() {
|
||||
|
||||
@@ -224,7 +224,7 @@ func GetStopwatches(ctx *context.APIContext) {
|
||||
return
|
||||
}
|
||||
|
||||
apiSWs, err := convert.ToStopWatches(ctx, sws)
|
||||
apiSWs, err := convert.ToStopWatches(ctx, ctx.Doer, sws)
|
||||
if err != nil {
|
||||
ctx.APIErrorInternal(err)
|
||||
return
|
||||
|
||||
@@ -1337,7 +1337,7 @@ func CancelScheduledAutoMerge(ctx *context.APIContext) {
|
||||
}
|
||||
|
||||
if ctx.Doer.ID != autoMerge.DoerID {
|
||||
allowed, err := access_model.IsUserRepoAdmin(ctx, ctx.Repo.Repository, ctx.Doer)
|
||||
allowed, err := pull_service.IsUserAllowedToMerge(ctx, pull, ctx.Repo.Permission, ctx.Doer)
|
||||
if err != nil {
|
||||
ctx.APIErrorInternal(err)
|
||||
return
|
||||
@@ -1548,9 +1548,9 @@ func GetPullRequestFiles(ctx *context.APIContext) {
|
||||
|
||||
var prInfo *pull_service.CompareInfo
|
||||
if pr.HasMerged {
|
||||
prInfo, err = pull_service.GetCompareInfo(ctx, pr.BaseRepo, pr.BaseRepo, baseGitRepo, pr.MergeBase, pr.GetGitHeadRefName(), true, false)
|
||||
prInfo, err = pull_service.GetCompareInfo(ctx, pr.BaseRepo, pr.BaseRepo, baseGitRepo, pr.MergeBase, pr.GetGitHeadRefName(), false, false)
|
||||
} else {
|
||||
prInfo, err = pull_service.GetCompareInfo(ctx, pr.BaseRepo, pr.BaseRepo, baseGitRepo, pr.BaseBranch, pr.GetGitHeadRefName(), true, false)
|
||||
prInfo, err = pull_service.GetCompareInfo(ctx, pr.BaseRepo, pr.BaseRepo, baseGitRepo, pr.BaseBranch, pr.GetGitHeadRefName(), false, false)
|
||||
}
|
||||
if err != nil {
|
||||
ctx.APIErrorInternal(err)
|
||||
|
||||
@@ -398,7 +398,6 @@ func DeleteReleaseAttachment(ctx *context.APIContext) {
|
||||
ctx.APIErrorNotFound()
|
||||
return
|
||||
}
|
||||
// FIXME Should prove the existence of the given repo, but results in unnecessary database requests
|
||||
|
||||
if err := repo_model.DeleteAttachment(ctx, attach, true); err != nil {
|
||||
ctx.APIErrorInternal(err)
|
||||
|
||||
@@ -44,9 +44,11 @@ func isRoutePathExpensive(routePattern string) bool {
|
||||
"/{username}/{reponame}/blame/",
|
||||
"/{username}/{reponame}/commit/",
|
||||
"/{username}/{reponame}/commits/",
|
||||
"/{username}/{reponame}/compare/",
|
||||
"/{username}/{reponame}/graph",
|
||||
"/{username}/{reponame}/media/",
|
||||
"/{username}/{reponame}/raw/",
|
||||
"/{username}/{reponame}/rss/branch/",
|
||||
"/{username}/{reponame}/src/",
|
||||
|
||||
// issue & PR related (no trailing slash)
|
||||
|
||||
@@ -230,8 +230,7 @@ func AuthorizeOAuth(ctx *context.Context) {
|
||||
|
||||
// pkce support
|
||||
switch form.CodeChallengeMethod {
|
||||
case "S256":
|
||||
case "plain":
|
||||
case "S256", "plain":
|
||||
if err := ctx.Session.Set("CodeChallengeMethod", form.CodeChallengeMethod); err != nil {
|
||||
handleAuthorizeError(ctx, AuthorizeError{
|
||||
ErrorCode: ErrorCodeServerError,
|
||||
|
||||
+28
-71
@@ -205,22 +205,24 @@ func ChangeProjectStatus(ctx *context.Context) {
|
||||
}
|
||||
id := ctx.PathParamInt64("id")
|
||||
|
||||
if err := project_model.ChangeProjectStatusByRepoIDAndID(ctx, 0, id, toClose); err != nil {
|
||||
ctx.NotFoundOrServerError("ChangeProjectStatusByRepoIDAndID", project_model.IsErrProjectNotExist, err)
|
||||
return
|
||||
}
|
||||
ctx.JSONRedirect(project_model.ProjectLinkForOrg(ctx.ContextUser, id))
|
||||
}
|
||||
|
||||
// DeleteProject delete a project
|
||||
func DeleteProject(ctx *context.Context) {
|
||||
p, err := project_model.GetProjectByID(ctx, ctx.PathParamInt64("id"))
|
||||
project, err := project_model.GetProjectByIDAndOwner(ctx, id, ctx.ContextUser.ID)
|
||||
if err != nil {
|
||||
ctx.NotFoundOrServerError("GetProjectByID", project_model.IsErrProjectNotExist, err)
|
||||
return
|
||||
}
|
||||
if p.OwnerID != ctx.ContextUser.ID {
|
||||
ctx.NotFound(nil)
|
||||
|
||||
if err := project_model.ChangeProjectStatusByRepoIDAndID(ctx, 0, project.ID, toClose); err != nil {
|
||||
ctx.NotFoundOrServerError("ChangeProjectStatusByRepoIDAndID", project_model.IsErrProjectNotExist, err)
|
||||
return
|
||||
}
|
||||
ctx.JSONRedirect(project_model.ProjectLinkForOrg(ctx.ContextUser, project.ID))
|
||||
}
|
||||
|
||||
// DeleteProject delete a project
|
||||
func DeleteProject(ctx *context.Context) {
|
||||
p, err := project_model.GetProjectByIDAndOwner(ctx, ctx.PathParamInt64("id"), ctx.ContextUser.ID)
|
||||
if err != nil {
|
||||
ctx.NotFoundOrServerError("GetProjectByID", project_model.IsErrProjectNotExist, err)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -246,15 +248,11 @@ func RenderEditProject(ctx *context.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
p, err := project_model.GetProjectByID(ctx, ctx.PathParamInt64("id"))
|
||||
p, err := project_model.GetProjectByIDAndOwner(ctx, ctx.PathParamInt64("id"), ctx.ContextUser.ID)
|
||||
if err != nil {
|
||||
ctx.NotFoundOrServerError("GetProjectByID", project_model.IsErrProjectNotExist, err)
|
||||
return
|
||||
}
|
||||
if p.OwnerID != ctx.ContextUser.ID {
|
||||
ctx.NotFound(nil)
|
||||
return
|
||||
}
|
||||
|
||||
ctx.Data["projectID"] = p.ID
|
||||
ctx.Data["title"] = p.Title
|
||||
@@ -288,15 +286,11 @@ func EditProjectPost(ctx *context.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
p, err := project_model.GetProjectByID(ctx, projectID)
|
||||
p, err := project_model.GetProjectByIDAndOwner(ctx, projectID, ctx.ContextUser.ID)
|
||||
if err != nil {
|
||||
ctx.NotFoundOrServerError("GetProjectByID", project_model.IsErrProjectNotExist, err)
|
||||
return
|
||||
}
|
||||
if p.OwnerID != ctx.ContextUser.ID {
|
||||
ctx.NotFound(nil)
|
||||
return
|
||||
}
|
||||
|
||||
p.Title = form.Title
|
||||
p.Description = form.Content
|
||||
@@ -316,15 +310,12 @@ func EditProjectPost(ctx *context.Context) {
|
||||
|
||||
// ViewProject renders the project with board view for a project
|
||||
func ViewProject(ctx *context.Context) {
|
||||
project, err := project_model.GetProjectByID(ctx, ctx.PathParamInt64("id"))
|
||||
project, err := project_model.GetProjectByIDAndOwner(ctx, ctx.PathParamInt64("id"), ctx.ContextUser.ID)
|
||||
if err != nil {
|
||||
ctx.NotFoundOrServerError("GetProjectByID", project_model.IsErrProjectNotExist, err)
|
||||
return
|
||||
}
|
||||
if project.OwnerID != ctx.ContextUser.ID {
|
||||
ctx.NotFound(nil)
|
||||
return
|
||||
}
|
||||
|
||||
if err := project.LoadOwner(ctx); err != nil {
|
||||
ctx.ServerError("LoadOwner", err)
|
||||
return
|
||||
@@ -455,28 +446,15 @@ func DeleteProjectColumn(ctx *context.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
project, err := project_model.GetProjectByID(ctx, ctx.PathParamInt64("id"))
|
||||
project, err := project_model.GetProjectByIDAndOwner(ctx, ctx.PathParamInt64("id"), ctx.ContextUser.ID)
|
||||
if err != nil {
|
||||
ctx.NotFoundOrServerError("GetProjectByID", project_model.IsErrProjectNotExist, err)
|
||||
return
|
||||
}
|
||||
|
||||
pb, err := project_model.GetColumn(ctx, ctx.PathParamInt64("columnID"))
|
||||
_, err = project_model.GetColumnByIDAndProjectID(ctx, ctx.PathParamInt64("columnID"), project.ID)
|
||||
if err != nil {
|
||||
ctx.ServerError("GetProjectColumn", err)
|
||||
return
|
||||
}
|
||||
if pb.ProjectID != ctx.PathParamInt64("id") {
|
||||
ctx.JSON(http.StatusUnprocessableEntity, map[string]string{
|
||||
"message": fmt.Sprintf("ProjectColumn[%d] is not in Project[%d] as expected", pb.ID, project.ID),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if project.OwnerID != ctx.ContextUser.ID {
|
||||
ctx.JSON(http.StatusUnprocessableEntity, map[string]string{
|
||||
"message": fmt.Sprintf("ProjectColumn[%d] is not in Owner[%d] as expected", pb.ID, ctx.ContextUser.ID),
|
||||
})
|
||||
ctx.NotFoundOrServerError("GetColumnByIDAndProjectID", project_model.IsErrProjectColumnNotExist, err)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -492,7 +470,7 @@ func DeleteProjectColumn(ctx *context.Context) {
|
||||
func AddColumnToProjectPost(ctx *context.Context) {
|
||||
form := web.GetForm(ctx).(*forms.EditProjectColumnForm)
|
||||
|
||||
project, err := project_model.GetProjectByID(ctx, ctx.PathParamInt64("id"))
|
||||
project, err := project_model.GetProjectByIDAndOwner(ctx, ctx.PathParamInt64("id"), ctx.ContextUser.ID)
|
||||
if err != nil {
|
||||
ctx.NotFoundOrServerError("GetProjectByID", project_model.IsErrProjectNotExist, err)
|
||||
return
|
||||
@@ -520,30 +498,18 @@ func CheckProjectColumnChangePermissions(ctx *context.Context) (*project_model.P
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
project, err := project_model.GetProjectByID(ctx, ctx.PathParamInt64("id"))
|
||||
project, err := project_model.GetProjectByIDAndOwner(ctx, ctx.PathParamInt64("id"), ctx.ContextUser.ID)
|
||||
if err != nil {
|
||||
ctx.NotFoundOrServerError("GetProjectByID", project_model.IsErrProjectNotExist, err)
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
column, err := project_model.GetColumn(ctx, ctx.PathParamInt64("columnID"))
|
||||
column, err := project_model.GetColumnByIDAndProjectID(ctx, ctx.PathParamInt64("columnID"), project.ID)
|
||||
if err != nil {
|
||||
ctx.ServerError("GetProjectColumn", err)
|
||||
return nil, nil
|
||||
}
|
||||
if column.ProjectID != ctx.PathParamInt64("id") {
|
||||
ctx.JSON(http.StatusUnprocessableEntity, map[string]string{
|
||||
"message": fmt.Sprintf("ProjectColumn[%d] is not in Project[%d] as expected", column.ID, project.ID),
|
||||
})
|
||||
ctx.NotFoundOrServerError("GetColumnByIDAndProjectID", project_model.IsErrProjectColumnNotExist, err)
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
if project.OwnerID != ctx.ContextUser.ID {
|
||||
ctx.JSON(http.StatusUnprocessableEntity, map[string]string{
|
||||
"message": fmt.Sprintf("ProjectColumn[%d] is not in Repository[%d] as expected", column.ID, project.ID),
|
||||
})
|
||||
return nil, nil
|
||||
}
|
||||
return project, column
|
||||
}
|
||||
|
||||
@@ -595,24 +561,15 @@ func MoveIssues(ctx *context.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
project, err := project_model.GetProjectByID(ctx, ctx.PathParamInt64("id"))
|
||||
project, err := project_model.GetProjectByIDAndOwner(ctx, ctx.PathParamInt64("id"), ctx.ContextUser.ID)
|
||||
if err != nil {
|
||||
ctx.NotFoundOrServerError("GetProjectByID", project_model.IsErrProjectNotExist, err)
|
||||
return
|
||||
}
|
||||
if project.OwnerID != ctx.ContextUser.ID {
|
||||
ctx.NotFound(nil)
|
||||
return
|
||||
}
|
||||
|
||||
column, err := project_model.GetColumn(ctx, ctx.PathParamInt64("columnID"))
|
||||
column, err := project_model.GetColumnByIDAndProjectID(ctx, ctx.PathParamInt64("columnID"), project.ID)
|
||||
if err != nil {
|
||||
ctx.NotFoundOrServerError("GetProjectColumn", project_model.IsErrProjectColumnNotExist, err)
|
||||
return
|
||||
}
|
||||
|
||||
if column.ProjectID != project.ID {
|
||||
ctx.NotFound(nil)
|
||||
ctx.NotFoundOrServerError("GetColumnByIDAndProjectID", project_model.IsErrProjectColumnNotExist, err)
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
@@ -4,11 +4,14 @@
|
||||
package org_test
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"testing"
|
||||
|
||||
"code.gitea.io/gitea/models/unittest"
|
||||
"code.gitea.io/gitea/modules/web"
|
||||
"code.gitea.io/gitea/routers/web/org"
|
||||
"code.gitea.io/gitea/services/contexttest"
|
||||
"code.gitea.io/gitea/services/forms"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
@@ -26,3 +29,30 @@ func TestCheckProjectColumnChangePermissions(t *testing.T) {
|
||||
assert.NotNil(t, column)
|
||||
assert.False(t, ctx.Written())
|
||||
}
|
||||
|
||||
func TestChangeProjectStatusRejectsForeignProjects(t *testing.T) {
|
||||
unittest.PrepareTestEnv(t)
|
||||
// project 4 is owned by user2 not user1
|
||||
ctx, _ := contexttest.MockContext(t, "user1/-/projects/4/close")
|
||||
contexttest.LoadUser(t, ctx, 1)
|
||||
ctx.ContextUser = ctx.Doer
|
||||
ctx.SetPathParam("action", "close")
|
||||
ctx.SetPathParam("id", "4")
|
||||
|
||||
org.ChangeProjectStatus(ctx)
|
||||
|
||||
assert.Equal(t, http.StatusNotFound, ctx.Resp.WrittenStatus())
|
||||
}
|
||||
|
||||
func TestAddColumnToProjectPostRejectsForeignProjects(t *testing.T) {
|
||||
unittest.PrepareTestEnv(t)
|
||||
ctx, _ := contexttest.MockContext(t, "user1/-/projects/4/columns/new")
|
||||
contexttest.LoadUser(t, ctx, 1)
|
||||
ctx.ContextUser = ctx.Doer
|
||||
ctx.SetPathParam("id", "4")
|
||||
web.SetForm(ctx, &forms.EditProjectColumnForm{Title: "foreign"})
|
||||
|
||||
org.AddColumnToProjectPost(ctx)
|
||||
|
||||
assert.Equal(t, http.StatusNotFound, ctx.Resp.WrittenStatus())
|
||||
}
|
||||
|
||||
@@ -4,11 +4,12 @@
|
||||
package repo
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
issues_model "code.gitea.io/gitea/models/issues"
|
||||
access_model "code.gitea.io/gitea/models/perm/access"
|
||||
repo_model "code.gitea.io/gitea/models/repo"
|
||||
"code.gitea.io/gitea/models/unit"
|
||||
"code.gitea.io/gitea/modules/httpcache"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
@@ -40,7 +41,7 @@ func uploadAttachment(ctx *context.Context, repoID int64, allowedTypes string) {
|
||||
|
||||
file, header, err := ctx.Req.FormFile("file")
|
||||
if err != nil {
|
||||
ctx.HTTPError(http.StatusInternalServerError, fmt.Sprintf("FormFile: %v", err))
|
||||
ctx.ServerError("FormFile", err)
|
||||
return
|
||||
}
|
||||
defer file.Close()
|
||||
@@ -56,7 +57,7 @@ func uploadAttachment(ctx *context.Context, repoID int64, allowedTypes string) {
|
||||
ctx.HTTPError(http.StatusBadRequest, err.Error())
|
||||
return
|
||||
}
|
||||
ctx.HTTPError(http.StatusInternalServerError, fmt.Sprintf("NewAttachment: %v", err))
|
||||
ctx.ServerError("UploadAttachmentGeneralSizeLimit", err)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -74,13 +75,44 @@ func DeleteAttachment(ctx *context.Context) {
|
||||
ctx.HTTPError(http.StatusBadRequest, err.Error())
|
||||
return
|
||||
}
|
||||
if !ctx.IsSigned || (ctx.Doer.ID != attach.UploaderID) {
|
||||
|
||||
if !ctx.IsSigned {
|
||||
ctx.HTTPError(http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
|
||||
if attach.RepoID != ctx.Repo.Repository.ID {
|
||||
ctx.HTTPError(http.StatusBadRequest, "attachment does not belong to this repository")
|
||||
return
|
||||
}
|
||||
|
||||
if ctx.Doer.ID != attach.UploaderID {
|
||||
if attach.IssueID > 0 {
|
||||
issue, err := issues_model.GetIssueByID(ctx, attach.IssueID)
|
||||
if err != nil {
|
||||
ctx.ServerError("GetIssueByID", err)
|
||||
return
|
||||
}
|
||||
if !ctx.Repo.Permission.CanWriteIssuesOrPulls(issue.IsPull) {
|
||||
ctx.HTTPError(http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
} else if attach.ReleaseID > 0 {
|
||||
if !ctx.Repo.Permission.CanWrite(unit.TypeReleases) {
|
||||
ctx.HTTPError(http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
} else {
|
||||
if !ctx.Repo.Permission.IsAdmin() && !ctx.Repo.Permission.IsOwner() {
|
||||
ctx.HTTPError(http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
err = repo_model.DeleteAttachment(ctx, attach, true)
|
||||
if err != nil {
|
||||
ctx.HTTPError(http.StatusInternalServerError, fmt.Sprintf("DeleteAttachment: %v", err))
|
||||
ctx.ServerError("DeleteAttachment", err)
|
||||
return
|
||||
}
|
||||
ctx.JSON(http.StatusOK, map[string]string{
|
||||
@@ -100,23 +132,41 @@ func ServeAttachment(ctx *context.Context, uuid string) {
|
||||
return
|
||||
}
|
||||
|
||||
repository, unitType, err := repo_service.LinkedRepository(ctx, attach)
|
||||
if err != nil {
|
||||
ctx.ServerError("LinkedRepository", err)
|
||||
// prevent visiting attachment from other repository directly
|
||||
// The check will be ignored before this code merged.
|
||||
if attach.CreatedUnix > repo_model.LegacyAttachmentMissingRepoIDCutoff && ctx.Repo.Repository != nil && ctx.Repo.Repository.ID != attach.RepoID {
|
||||
ctx.HTTPError(http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
if repository == nil { // If not linked
|
||||
unitType, repoID, err := repo_service.GetAttachmentLinkedTypeAndRepoID(ctx, attach)
|
||||
if err != nil {
|
||||
ctx.ServerError("GetAttachmentLinkedTypeAndRepoID", err)
|
||||
return
|
||||
}
|
||||
|
||||
if unitType == unit.TypeInvalid { // unlinked attachment can only be accessed by the uploader
|
||||
if !(ctx.IsSigned && attach.UploaderID == ctx.Doer.ID) { // We block if not the uploader
|
||||
ctx.HTTPError(http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
} else { // If we have the repository we check access
|
||||
perm, err := access_model.GetUserRepoPermission(ctx, repository, ctx.Doer)
|
||||
if err != nil {
|
||||
ctx.HTTPError(http.StatusInternalServerError, "GetUserRepoPermission", err.Error())
|
||||
return
|
||||
} else { // If we have the linked type, we need to check access
|
||||
var perm access_model.Permission
|
||||
if ctx.Repo.Repository == nil {
|
||||
repo, err := repo_model.GetRepositoryByID(ctx, repoID)
|
||||
if err != nil {
|
||||
ctx.ServerError("GetRepositoryByID", err)
|
||||
return
|
||||
}
|
||||
perm, err = access_model.GetUserRepoPermission(ctx, repo, ctx.Doer)
|
||||
if err != nil {
|
||||
ctx.ServerError("GetUserRepoPermission", err)
|
||||
return
|
||||
}
|
||||
} else {
|
||||
perm = ctx.Repo.Permission
|
||||
}
|
||||
|
||||
if !perm.CanRead(unitType) {
|
||||
ctx.HTTPError(http.StatusNotFound)
|
||||
return
|
||||
|
||||
+18
-29
@@ -31,6 +31,7 @@ import (
|
||||
repo_module "code.gitea.io/gitea/modules/repository"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/modules/structs"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
"code.gitea.io/gitea/services/context"
|
||||
repo_service "code.gitea.io/gitea/services/repository"
|
||||
|
||||
@@ -57,7 +58,7 @@ func CorsHandler() func(next http.Handler) http.Handler {
|
||||
}
|
||||
|
||||
// httpBase implementation git smart HTTP protocol
|
||||
func httpBase(ctx *context.Context) *serviceHandler {
|
||||
func httpBase(ctx *context.Context, optGitService ...string) *serviceHandler {
|
||||
username := ctx.PathParam("username")
|
||||
reponame := strings.TrimSuffix(ctx.PathParam("reponame"), ".git")
|
||||
|
||||
@@ -67,18 +68,14 @@ func httpBase(ctx *context.Context) *serviceHandler {
|
||||
}
|
||||
|
||||
var isPull, receivePack bool
|
||||
service := ctx.FormString("service")
|
||||
if service == "git-receive-pack" ||
|
||||
strings.HasSuffix(ctx.Req.URL.Path, "git-receive-pack") {
|
||||
isPull = false
|
||||
switch util.OptionalArg(optGitService) {
|
||||
case "git-receive-pack":
|
||||
receivePack = true
|
||||
} else if service == "git-upload-pack" ||
|
||||
strings.HasSuffix(ctx.Req.URL.Path, "git-upload-pack") {
|
||||
case "git-upload-pack":
|
||||
isPull = true
|
||||
} else if service == "git-upload-archive" ||
|
||||
strings.HasSuffix(ctx.Req.URL.Path, "git-upload-archive") {
|
||||
case "git-upload-archive":
|
||||
isPull = true
|
||||
} else {
|
||||
default:
|
||||
isPull = ctx.Req.Method == http.MethodHead || ctx.Req.Method == http.MethodGet
|
||||
}
|
||||
|
||||
@@ -411,13 +408,19 @@ func prepareGitCmdWithAllowedService(service string) (*gitcmd.Command, error) {
|
||||
return nil, fmt.Errorf("service %q is not allowed", service)
|
||||
}
|
||||
|
||||
func serviceRPC(ctx *context.Context, h *serviceHandler, service string) {
|
||||
func serviceRPC(ctx *context.Context, service string) {
|
||||
defer func() {
|
||||
if err := ctx.Req.Body.Close(); err != nil {
|
||||
log.Error("serviceRPC: Close: %v", err)
|
||||
}
|
||||
}()
|
||||
|
||||
h := httpBase(ctx, "git-"+service)
|
||||
if h == nil {
|
||||
ctx.Resp.WriteHeader(http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
expectedContentType := fmt.Sprintf("application/x-git-%s-request", service)
|
||||
if ctx.Req.Header.Get("Content-Type") != expectedContentType {
|
||||
log.Error("Content-Type (%q) doesn't match expected: %q", ctx.Req.Header.Get("Content-Type"), expectedContentType)
|
||||
@@ -472,26 +475,12 @@ func serviceRPC(ctx *context.Context, h *serviceHandler, service string) {
|
||||
|
||||
// ServiceUploadPack implements Git Smart HTTP protocol
|
||||
func ServiceUploadPack(ctx *context.Context) {
|
||||
h := httpBase(ctx)
|
||||
if h != nil {
|
||||
serviceRPC(ctx, h, "upload-pack")
|
||||
}
|
||||
serviceRPC(ctx, "upload-pack")
|
||||
}
|
||||
|
||||
// ServiceReceivePack implements Git Smart HTTP protocol
|
||||
func ServiceReceivePack(ctx *context.Context) {
|
||||
h := httpBase(ctx)
|
||||
if h != nil {
|
||||
serviceRPC(ctx, h, "receive-pack")
|
||||
}
|
||||
}
|
||||
|
||||
func getServiceType(ctx *context.Context) string {
|
||||
serviceType := ctx.Req.FormValue("service")
|
||||
if !strings.HasPrefix(serviceType, "git-") {
|
||||
return ""
|
||||
}
|
||||
return strings.TrimPrefix(serviceType, "git-")
|
||||
serviceRPC(ctx, "receive-pack")
|
||||
}
|
||||
|
||||
func updateServerInfo(ctx gocontext.Context, dir string) []byte {
|
||||
@@ -512,12 +501,12 @@ func packetWrite(str string) []byte {
|
||||
|
||||
// GetInfoRefs implements Git dumb HTTP
|
||||
func GetInfoRefs(ctx *context.Context) {
|
||||
h := httpBase(ctx)
|
||||
service := strings.TrimPrefix(ctx.Req.FormValue("service"), "git-")
|
||||
h := httpBase(ctx, "git-"+service)
|
||||
if h == nil {
|
||||
return
|
||||
}
|
||||
setHeaderNoCache(ctx)
|
||||
service := getServiceType(ctx)
|
||||
cmd, err := prepareGitCmdWithAllowedService(service)
|
||||
if err == nil {
|
||||
if protocol := ctx.Req.Header.Get("Git-Protocol"); protocol != "" && safeGitProtocolHeader.MatchString(protocol) {
|
||||
|
||||
@@ -32,6 +32,7 @@ import (
|
||||
"code.gitea.io/gitea/modules/graceful"
|
||||
issue_template "code.gitea.io/gitea/modules/issue/template"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
"code.gitea.io/gitea/modules/optional"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/modules/templates"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
@@ -1100,11 +1101,9 @@ func MergePullRequest(ctx *context.Context) {
|
||||
message += "\n\n" + form.MergeMessageField
|
||||
}
|
||||
|
||||
deleteBranchAfterMerge, err := pull_service.ShouldDeleteBranchAfterMerge(ctx, form.DeleteBranchAfterMerge, ctx.Repo.Repository, pr)
|
||||
if err != nil {
|
||||
ctx.ServerError("ShouldDeleteBranchAfterMerge", err)
|
||||
return
|
||||
}
|
||||
// There is always a checkbox on the UI (the DeleteBranchAfterMerge is nil if the checkbox is not checked),
|
||||
// just use the user's choice, don't use pull_service.ShouldDeleteBranchAfterMerge to decide
|
||||
deleteBranchAfterMerge := optional.FromPtr(form.DeleteBranchAfterMerge).Value()
|
||||
|
||||
if form.MergeWhenChecksSucceed {
|
||||
// delete all scheduled auto merges
|
||||
@@ -1230,6 +1229,28 @@ func CancelAutoMergePullRequest(ctx *context.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
exist, autoMerge, err := pull_model.GetScheduledMergeByPullID(ctx, issue.PullRequest.ID)
|
||||
if err != nil {
|
||||
ctx.ServerError("GetScheduledMergeByPullID", err)
|
||||
return
|
||||
}
|
||||
if !exist {
|
||||
ctx.NotFound(nil)
|
||||
return
|
||||
}
|
||||
|
||||
if ctx.Doer.ID != autoMerge.DoerID {
|
||||
allowed, err := pull_service.IsUserAllowedToMerge(ctx, issue.PullRequest, ctx.Repo.Permission, ctx.Doer)
|
||||
if err != nil {
|
||||
ctx.ServerError("IsUserAllowedToMerge", err)
|
||||
return
|
||||
}
|
||||
if !allowed {
|
||||
ctx.HTTPError(http.StatusForbidden, "user has no permission to cancel the scheduled auto merge")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if err := automerge.RemoveScheduledAutoMerge(ctx, ctx.Doer, issue.PullRequest); err != nil {
|
||||
if db.IsErrNotExist(err) {
|
||||
ctx.Flash.Error(ctx.Tr("repo.pulls.auto_merge_not_scheduled"))
|
||||
|
||||
@@ -15,6 +15,7 @@ import (
|
||||
repo_model "code.gitea.io/gitea/models/repo"
|
||||
"code.gitea.io/gitea/models/unit"
|
||||
"code.gitea.io/gitea/modules/base"
|
||||
"code.gitea.io/gitea/modules/container"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
"code.gitea.io/gitea/modules/optional"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
@@ -128,7 +129,9 @@ func prepareUserNotificationsData(ctx *context.Context) {
|
||||
ctx.Data["Notifications"] = notifications
|
||||
ctx.Data["Link"] = setting.AppSubURL + "/notifications"
|
||||
ctx.Data["SequenceNumber"] = ctx.FormString("sequence-number")
|
||||
|
||||
pager.AddParamFromRequest(ctx.Req)
|
||||
pager.RemoveParam(container.SetOf("div-only", "sequence-number"))
|
||||
ctx.Data["Page"] = pager
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
// Copyright 2024 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package security
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"code.gitea.io/gitea/models/unittest"
|
||||
)
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
unittest.MainTest(m)
|
||||
}
|
||||
@@ -4,12 +4,14 @@
|
||||
package security
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
"code.gitea.io/gitea/modules/auth/openid"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
"code.gitea.io/gitea/modules/web"
|
||||
"code.gitea.io/gitea/services/context"
|
||||
"code.gitea.io/gitea/services/forms"
|
||||
@@ -116,7 +118,11 @@ func DeleteOpenID(ctx *context.Context) {
|
||||
}
|
||||
|
||||
if err := user_model.DeleteUserOpenID(ctx, &user_model.UserOpenID{ID: ctx.FormInt64("id"), UID: ctx.Doer.ID}); err != nil {
|
||||
ctx.ServerError("DeleteUserOpenID", err)
|
||||
if errors.Is(err, util.ErrNotExist) {
|
||||
ctx.HTTPError(http.StatusNotFound)
|
||||
} else {
|
||||
ctx.ServerError("DeleteUserOpenID", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
log.Trace("OpenID address deleted: %s", ctx.Doer.Name)
|
||||
@@ -132,8 +138,12 @@ func ToggleOpenIDVisibility(ctx *context.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
if err := user_model.ToggleUserOpenIDVisibility(ctx, ctx.FormInt64("id")); err != nil {
|
||||
ctx.ServerError("ToggleUserOpenIDVisibility", err)
|
||||
if err := user_model.ToggleUserOpenIDVisibility(ctx, ctx.FormInt64("id"), ctx.Doer); err != nil {
|
||||
if errors.Is(err, util.ErrNotExist) {
|
||||
ctx.HTTPError(http.StatusNotFound)
|
||||
} else {
|
||||
ctx.ServerError("ToggleUserOpenIDVisibility", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
// Copyright 2024 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package security
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"testing"
|
||||
|
||||
"code.gitea.io/gitea/models/unittest"
|
||||
"code.gitea.io/gitea/services/contexttest"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestDeleteOpenIDReturnsNotFoundForOtherUsersAddress(t *testing.T) {
|
||||
unittest.PrepareTestEnv(t)
|
||||
ctx, _ := contexttest.MockContext(t, "POST /user/settings/security")
|
||||
contexttest.LoadUser(t, ctx, 2)
|
||||
ctx.SetFormString("id", "1")
|
||||
|
||||
DeleteOpenID(ctx)
|
||||
|
||||
assert.Equal(t, http.StatusNotFound, ctx.Resp.WrittenStatus())
|
||||
}
|
||||
|
||||
func TestToggleOpenIDVisibilityReturnsNotFoundForOtherUsersAddress(t *testing.T) {
|
||||
unittest.PrepareTestEnv(t)
|
||||
ctx, _ := contexttest.MockContext(t, "POST /user/settings/security")
|
||||
contexttest.LoadUser(t, ctx, 2)
|
||||
ctx.SetFormString("id", "1")
|
||||
|
||||
ToggleOpenIDVisibility(ctx)
|
||||
|
||||
assert.Equal(t, http.StatusNotFound, ctx.Resp.WrittenStatus())
|
||||
}
|
||||
@@ -29,7 +29,7 @@ func GetStopwatches(ctx *context.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
apiSWs, err := convert.ToStopWatches(ctx, sws)
|
||||
apiSWs, err := convert.ToStopWatches(ctx, ctx.Doer, sws)
|
||||
if err != nil {
|
||||
ctx.HTTPError(http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"path"
|
||||
"strings"
|
||||
|
||||
actions_model "code.gitea.io/gitea/models/actions"
|
||||
"code.gitea.io/gitea/models/db"
|
||||
@@ -91,6 +92,7 @@ func createCommitStatus(ctx context.Context, job *actions_model.ActionRunJob) er
|
||||
runName = wfs[0].Name
|
||||
}
|
||||
ctxname := fmt.Sprintf("%s / %s (%s)", runName, job.Name, event)
|
||||
ctxname = strings.TrimSpace(ctxname) // git_model.NewCommitStatus also trims spaces
|
||||
state := toCommitStatus(job.Status)
|
||||
if statuses, err := git_model.GetLatestCommitStatus(ctx, repo.ID, sha, db.ListOptionsAll); err == nil {
|
||||
for _, v := range statuses {
|
||||
|
||||
@@ -8,8 +8,10 @@ import (
|
||||
"html/template"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
"code.gitea.io/gitea/modules/container"
|
||||
"code.gitea.io/gitea/modules/paginator"
|
||||
)
|
||||
|
||||
@@ -49,6 +51,14 @@ func (p *Pagination) AddParamFromRequest(req *http.Request) {
|
||||
p.AddParamFromQuery(req.URL.Query())
|
||||
}
|
||||
|
||||
func (p *Pagination) RemoveParam(keys container.Set[string]) {
|
||||
p.urlParams = slices.DeleteFunc(p.urlParams, func(s string) bool {
|
||||
k, _, _ := strings.Cut(s, "=")
|
||||
k, _ = url.QueryUnescape(k)
|
||||
return keys.Contains(k)
|
||||
})
|
||||
}
|
||||
|
||||
// GetParams returns the configured URL params
|
||||
func (p *Pagination) GetParams() template.URL {
|
||||
return template.URL(strings.Join(p.urlParams, "&"))
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package context
|
||||
|
||||
import (
|
||||
"net/url"
|
||||
"testing"
|
||||
|
||||
"code.gitea.io/gitea/modules/container"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestPagination(t *testing.T) {
|
||||
p := NewPagination(1, 1, 1, 1)
|
||||
params := url.Values{}
|
||||
params.Add("k1", "11")
|
||||
params.Add("k1", "12")
|
||||
params.Add("k", "a")
|
||||
params.Add("k", "b")
|
||||
params.Add("k2", "21")
|
||||
params.Add("k2", "22")
|
||||
params.Add("foo", "bar")
|
||||
|
||||
p.AddParamFromQuery(params)
|
||||
v, _ := url.ParseQuery(string(p.GetParams()))
|
||||
assert.Equal(t, params, v)
|
||||
|
||||
p.RemoveParam(container.SetOf("k", "foo"))
|
||||
params.Del("k")
|
||||
params.Del("foo")
|
||||
v, _ = url.ParseQuery(string(p.GetParams()))
|
||||
assert.Equal(t, params, v)
|
||||
}
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
"strings"
|
||||
|
||||
issues_model "code.gitea.io/gitea/models/issues"
|
||||
access_model "code.gitea.io/gitea/models/perm/access"
|
||||
repo_model "code.gitea.io/gitea/models/repo"
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
"code.gitea.io/gitea/modules/label"
|
||||
@@ -163,11 +164,12 @@ func ToTrackedTime(ctx context.Context, doer *user_model.User, t *issues_model.T
|
||||
}
|
||||
|
||||
// ToStopWatches convert Stopwatch list to api.StopWatches
|
||||
func ToStopWatches(ctx context.Context, sws []*issues_model.Stopwatch) (api.StopWatches, error) {
|
||||
func ToStopWatches(ctx context.Context, doer *user_model.User, sws []*issues_model.Stopwatch) (api.StopWatches, error) {
|
||||
result := api.StopWatches(make([]api.StopWatch, 0, len(sws)))
|
||||
|
||||
issueCache := make(map[int64]*issues_model.Issue)
|
||||
repoCache := make(map[int64]*repo_model.Repository)
|
||||
permCache := make(map[int64]access_model.Permission)
|
||||
var (
|
||||
issue *issues_model.Issue
|
||||
repo *repo_model.Repository
|
||||
@@ -182,13 +184,30 @@ func ToStopWatches(ctx context.Context, sws []*issues_model.Stopwatch) (api.Stop
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
issueCache[sw.IssueID] = issue
|
||||
}
|
||||
repo, ok = repoCache[issue.RepoID]
|
||||
if !ok {
|
||||
repo, err = repo_model.GetRepositoryByID(ctx, issue.RepoID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
log.Error("GetRepositoryByID(%d): %v", issue.RepoID, err)
|
||||
continue
|
||||
}
|
||||
repoCache[issue.RepoID] = repo
|
||||
}
|
||||
|
||||
// ADD: Check user permissions
|
||||
perm, ok := permCache[repo.ID]
|
||||
if !ok {
|
||||
perm, err = access_model.GetUserRepoPermission(ctx, repo, doer)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
permCache[repo.ID] = perm
|
||||
}
|
||||
|
||||
if !perm.CanReadIssuesOrPulls(issue.IsPull) {
|
||||
continue
|
||||
}
|
||||
|
||||
result = append(result, api.StopWatch{
|
||||
|
||||
@@ -8,9 +8,11 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"code.gitea.io/gitea/models/db"
|
||||
issues_model "code.gitea.io/gitea/models/issues"
|
||||
repo_model "code.gitea.io/gitea/models/repo"
|
||||
"code.gitea.io/gitea/models/unittest"
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
api "code.gitea.io/gitea/modules/structs"
|
||||
"code.gitea.io/gitea/modules/timeutil"
|
||||
@@ -55,3 +57,29 @@ func TestMilestone_APIFormat(t *testing.T) {
|
||||
Deadline: milestone.DeadlineUnix.AsTimePtr(),
|
||||
}, *ToAPIMilestone(milestone))
|
||||
}
|
||||
|
||||
func TestToStopWatchesRespectsPermissions(t *testing.T) {
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
|
||||
ctx := t.Context()
|
||||
publicSW := unittest.AssertExistsAndLoadBean(t, &issues_model.Stopwatch{ID: 1})
|
||||
privateIssue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{RepoID: 3})
|
||||
privateSW := &issues_model.Stopwatch{IssueID: privateIssue.ID, UserID: 5}
|
||||
assert.NoError(t, db.Insert(ctx, privateSW))
|
||||
assert.NotZero(t, privateSW.ID)
|
||||
|
||||
regularUser := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 5})
|
||||
adminUser := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1})
|
||||
|
||||
sws := []*issues_model.Stopwatch{publicSW, privateSW}
|
||||
|
||||
visible, err := ToStopWatches(ctx, regularUser, sws)
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, visible, 1)
|
||||
assert.Equal(t, "repo1", visible[0].RepoName)
|
||||
|
||||
visibleAdmin, err := ToStopWatches(ctx, adminUser, sws)
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, visibleAdmin, 2)
|
||||
assert.ElementsMatch(t, []string{"repo1", "repo3"}, []string{visibleAdmin[0].RepoName, visibleAdmin[1].RepoName})
|
||||
}
|
||||
|
||||
@@ -8,8 +8,8 @@ import (
|
||||
"net/url"
|
||||
|
||||
activities_model "code.gitea.io/gitea/models/activities"
|
||||
"code.gitea.io/gitea/models/perm"
|
||||
access_model "code.gitea.io/gitea/models/perm/access"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
api "code.gitea.io/gitea/modules/structs"
|
||||
)
|
||||
|
||||
@@ -25,11 +25,17 @@ func ToNotificationThread(ctx context.Context, n *activities_model.Notification)
|
||||
|
||||
// since user only get notifications when he has access to use minimal access mode
|
||||
if n.Repository != nil {
|
||||
result.Repository = ToRepo(ctx, n.Repository, access_model.Permission{AccessMode: perm.AccessModeRead})
|
||||
|
||||
// This permission is not correct and we should not be reporting it
|
||||
for repository := result.Repository; repository != nil; repository = repository.Parent {
|
||||
repository.Permissions = nil
|
||||
perm, err := access_model.GetUserRepoPermission(ctx, n.Repository, n.User)
|
||||
if err != nil {
|
||||
log.Error("GetUserRepoPermission failed: %v", err)
|
||||
return result
|
||||
}
|
||||
if perm.HasAnyUnitAccessOrPublicAccess() { // if user has been revoked access to repo, do not show repo info
|
||||
result.Repository = ToRepo(ctx, n.Repository, perm)
|
||||
// This permission is not correct and we should not be reporting it
|
||||
for repository := result.Repository; repository != nil; repository = repository.Parent {
|
||||
repository.Permissions = nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package convert
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
activities_model "code.gitea.io/gitea/models/activities"
|
||||
repo_model "code.gitea.io/gitea/models/repo"
|
||||
"code.gitea.io/gitea/models/unittest"
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
"code.gitea.io/gitea/modules/timeutil"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestToNotificationThreadIncludesRepoForAccessibleUser(t *testing.T) {
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
|
||||
n := newRepoNotification(t, 1, 4)
|
||||
thread := ToNotificationThread(t.Context(), n)
|
||||
|
||||
if assert.NotNil(t, thread.Repository) {
|
||||
assert.Equal(t, n.Repository.FullName(), thread.Repository.FullName)
|
||||
assert.Nil(t, thread.Repository.Permissions)
|
||||
}
|
||||
}
|
||||
|
||||
func TestToNotificationThreadOmitsRepoWhenAccessRevoked(t *testing.T) {
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
|
||||
n := newRepoNotification(t, 2, 4)
|
||||
thread := ToNotificationThread(t.Context(), n)
|
||||
|
||||
assert.Nil(t, thread.Repository)
|
||||
}
|
||||
|
||||
func newRepoNotification(t *testing.T, repoID, userID int64) *activities_model.Notification {
|
||||
t.Helper()
|
||||
|
||||
ctx := t.Context()
|
||||
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: repoID})
|
||||
assert.NoError(t, repo.LoadOwner(ctx))
|
||||
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: userID})
|
||||
|
||||
return &activities_model.Notification{
|
||||
ID: repoID*1000 + userID,
|
||||
UserID: user.ID,
|
||||
RepoID: repo.ID,
|
||||
Status: activities_model.NotificationStatusUnread,
|
||||
Source: activities_model.NotificationSourceRepository,
|
||||
UpdatedUnix: timeutil.TimeStampNow(),
|
||||
Repository: repo,
|
||||
User: user,
|
||||
}
|
||||
}
|
||||
@@ -127,20 +127,10 @@ func innerToRepo(ctx context.Context, repo *repo_model.Repository, permissionInR
|
||||
projectsMode = config.ProjectsMode
|
||||
}
|
||||
|
||||
hasReleases := false
|
||||
if _, err := repo.GetUnit(ctx, unit_model.TypeReleases); err == nil {
|
||||
hasReleases = true
|
||||
}
|
||||
|
||||
hasPackages := false
|
||||
if _, err := repo.GetUnit(ctx, unit_model.TypePackages); err == nil {
|
||||
hasPackages = true
|
||||
}
|
||||
|
||||
hasActions := false
|
||||
if _, err := repo.GetUnit(ctx, unit_model.TypeActions); err == nil {
|
||||
hasActions = true
|
||||
}
|
||||
hasCode := repo.UnitEnabled(ctx, unit_model.TypeCode)
|
||||
hasReleases := repo.UnitEnabled(ctx, unit_model.TypeReleases)
|
||||
hasPackages := repo.UnitEnabled(ctx, unit_model.TypePackages)
|
||||
hasActions := repo.UnitEnabled(ctx, unit_model.TypeActions)
|
||||
|
||||
if err := repo.LoadOwner(ctx); err != nil {
|
||||
return nil
|
||||
@@ -221,6 +211,7 @@ func innerToRepo(ctx context.Context, repo *repo_model.Repository, permissionInR
|
||||
Updated: repo.UpdatedUnix.AsTime(),
|
||||
ArchivedAt: repo.ArchivedUnix.AsTime(),
|
||||
Permissions: permission,
|
||||
HasCode: hasCode,
|
||||
HasIssues: hasIssues,
|
||||
ExternalTracker: externalTracker,
|
||||
InternalTracker: internalTracker,
|
||||
|
||||
@@ -90,7 +90,7 @@ func GetListLockHandler(ctx *context.Context) {
|
||||
})
|
||||
return
|
||||
}
|
||||
lock, err := git_model.GetLFSLockByID(ctx, v)
|
||||
lock, err := git_model.GetLFSLockByIDAndRepo(ctx, v, repository.ID)
|
||||
if err != nil && !git_model.IsErrLFSLockNotExist(err) {
|
||||
log.Error("Unable to get lock with ID[%s]: Error: %v", v, err)
|
||||
}
|
||||
|
||||
+10
-6
@@ -14,7 +14,6 @@ import (
|
||||
"maps"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"path"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
@@ -29,6 +28,7 @@ import (
|
||||
"code.gitea.io/gitea/models/unit"
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
"code.gitea.io/gitea/modules/auth/httpauth"
|
||||
"code.gitea.io/gitea/modules/httplib"
|
||||
"code.gitea.io/gitea/modules/json"
|
||||
lfs_module "code.gitea.io/gitea/modules/lfs"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
@@ -45,6 +45,7 @@ type requestContext struct {
|
||||
Repo string
|
||||
Authorization string
|
||||
Method string
|
||||
RepoGitURL string
|
||||
}
|
||||
|
||||
// Claims is a JWT Token Claims
|
||||
@@ -84,17 +85,17 @@ func GetLFSAuthTokenWithBearer(opts AuthTokenOptions) (string, error) {
|
||||
|
||||
// DownloadLink builds a URL to download the object.
|
||||
func (rc *requestContext) DownloadLink(p lfs_module.Pointer) string {
|
||||
return setting.AppURL + path.Join(url.PathEscape(rc.User), url.PathEscape(rc.Repo+".git"), "info/lfs/objects", url.PathEscape(p.Oid))
|
||||
return rc.RepoGitURL + "/info/lfs/objects/" + url.PathEscape(p.Oid)
|
||||
}
|
||||
|
||||
// UploadLink builds a URL to upload the object.
|
||||
func (rc *requestContext) UploadLink(p lfs_module.Pointer) string {
|
||||
return setting.AppURL + path.Join(url.PathEscape(rc.User), url.PathEscape(rc.Repo+".git"), "info/lfs/objects", url.PathEscape(p.Oid), strconv.FormatInt(p.Size, 10))
|
||||
return rc.RepoGitURL + "/info/lfs/objects/" + url.PathEscape(p.Oid) + "/" + strconv.FormatInt(p.Size, 10)
|
||||
}
|
||||
|
||||
// VerifyLink builds a URL for verifying the object.
|
||||
func (rc *requestContext) VerifyLink(p lfs_module.Pointer) string {
|
||||
return setting.AppURL + path.Join(url.PathEscape(rc.User), url.PathEscape(rc.Repo+".git"), "info/lfs/verify")
|
||||
return rc.RepoGitURL + "/info/lfs/verify"
|
||||
}
|
||||
|
||||
// CheckAcceptMediaType checks if the client accepts the LFS media type.
|
||||
@@ -422,11 +423,14 @@ func decodeJSON(req *http.Request, v any) error {
|
||||
}
|
||||
|
||||
func getRequestContext(ctx *context.Context) *requestContext {
|
||||
ownerName := ctx.PathParam("username")
|
||||
repoName := strings.TrimSuffix(ctx.PathParam("reponame"), ".git")
|
||||
return &requestContext{
|
||||
User: ctx.PathParam("username"),
|
||||
Repo: strings.TrimSuffix(ctx.PathParam("reponame"), ".git"),
|
||||
User: ownerName,
|
||||
Repo: repoName,
|
||||
Authorization: ctx.Req.Header.Get("Authorization"),
|
||||
Method: ctx.Req.Method,
|
||||
RepoGitURL: httplib.GuessCurrentAppURL(ctx) + url.PathEscape(ownerName) + "/" + url.PathEscape(repoName+".git"),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -7,9 +7,12 @@ import (
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"slices"
|
||||
|
||||
access_model "code.gitea.io/gitea/models/perm/access"
|
||||
"code.gitea.io/gitea/models/renderhelper"
|
||||
repo_model "code.gitea.io/gitea/models/repo"
|
||||
"code.gitea.io/gitea/models/unit"
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
"code.gitea.io/gitea/modules/markup/markdown"
|
||||
@@ -44,6 +47,16 @@ func MailNewRelease(ctx context.Context, rel *repo_model.Release) {
|
||||
return
|
||||
}
|
||||
|
||||
if err := rel.LoadRepo(ctx); err != nil {
|
||||
log.Error("rel.LoadRepo: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
// delete publisher or any users with no permission
|
||||
recipients = slices.DeleteFunc(recipients, func(u *user_model.User) bool {
|
||||
return u.ID == rel.PublisherID || !access_model.CheckRepoUnitUser(ctx, rel.Repo, u, unit.TypeReleases)
|
||||
})
|
||||
|
||||
langMap := make(map[string][]*user_model.User)
|
||||
for _, user := range recipients {
|
||||
if user.ID != rel.PublisherID {
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package mailer
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
repo_model "code.gitea.io/gitea/models/repo"
|
||||
"code.gitea.io/gitea/models/unittest"
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
sender_service "code.gitea.io/gitea/services/mailer/sender"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestMailNewReleaseFiltersUnauthorizedWatchers(t *testing.T) {
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
|
||||
origMailService := setting.MailService
|
||||
origDomain := setting.Domain
|
||||
origAppName := setting.AppName
|
||||
origAppURL := setting.AppURL
|
||||
origTemplates := LoadedTemplates()
|
||||
defer func() {
|
||||
setting.MailService = origMailService
|
||||
setting.Domain = origDomain
|
||||
setting.AppName = origAppName
|
||||
setting.AppURL = origAppURL
|
||||
loadedTemplates.Store(origTemplates)
|
||||
}()
|
||||
|
||||
setting.MailService = &setting.Mailer{
|
||||
From: "Gitea",
|
||||
FromEmail: "noreply@example.com",
|
||||
}
|
||||
setting.Domain = "example.com"
|
||||
setting.AppName = "Gitea"
|
||||
setting.AppURL = "https://example.com/"
|
||||
prepareMailTemplates(string(tplNewReleaseMail), "{{.Subject}}", "<p>{{.Release.TagName}}</p>")
|
||||
|
||||
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 2})
|
||||
require.True(t, repo.IsPrivate)
|
||||
|
||||
admin := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1})
|
||||
unauthorized := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 5})
|
||||
|
||||
assert.NoError(t, repo_model.WatchRepo(t.Context(), admin, repo, true))
|
||||
assert.NoError(t, repo_model.WatchRepo(t.Context(), unauthorized, repo, true))
|
||||
|
||||
rel := unittest.AssertExistsAndLoadBean(t, &repo_model.Release{ID: 11})
|
||||
rel.Repo = nil
|
||||
rel.Publisher = unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: rel.PublisherID})
|
||||
|
||||
var sent []*sender_service.Message
|
||||
origSend := SendAsync
|
||||
SendAsync = func(msgs ...*sender_service.Message) {
|
||||
sent = append(sent, msgs...)
|
||||
}
|
||||
defer func() {
|
||||
SendAsync = origSend
|
||||
}()
|
||||
|
||||
MailNewRelease(t.Context(), rel)
|
||||
|
||||
require.Len(t, sent, 1)
|
||||
assert.Equal(t, admin.EmailTo(), sent[0].To)
|
||||
assert.NotEqual(t, unauthorized.EmailTo(), sent[0].To)
|
||||
}
|
||||
@@ -320,6 +320,7 @@ func (g *GiteaLocalUploader) CreateReleases(ctx context.Context, releases ...*ba
|
||||
}
|
||||
attach := repo_model.Attachment{
|
||||
UUID: uuid.New().String(),
|
||||
RepoID: g.repo.ID,
|
||||
Name: asset.Name,
|
||||
DownloadCount: int64(*asset.DownloadCount),
|
||||
Size: int64(*asset.Size),
|
||||
|
||||
@@ -329,7 +329,6 @@ func (g *GithubDownloaderV3) convertGithubRelease(ctx context.Context, rel *gith
|
||||
r.Assets = append(r.Assets, &base.ReleaseAsset{
|
||||
ID: asset.GetID(),
|
||||
Name: asset.GetName(),
|
||||
ContentType: asset.ContentType,
|
||||
Size: asset.Size,
|
||||
DownloadCount: asset.DownloadCount,
|
||||
Created: asset.CreatedAt.Time,
|
||||
|
||||
@@ -316,12 +316,11 @@ func (g *GitlabDownloader) convertGitlabRelease(ctx context.Context, rel *gitlab
|
||||
|
||||
httpClient := NewMigrationHTTPClient()
|
||||
|
||||
for k, asset := range rel.Assets.Links {
|
||||
for _, asset := range rel.Assets.Links {
|
||||
assetID := asset.ID // Don't optimize this, for closure we need a local variable
|
||||
r.Assets = append(r.Assets, &base.ReleaseAsset{
|
||||
ID: int64(asset.ID),
|
||||
Name: asset.Name,
|
||||
ContentType: &rel.Assets.Sources[k].Format,
|
||||
Size: &zero,
|
||||
DownloadCount: &zero,
|
||||
DownloadFunc: func() (io.ReadCloser, error) {
|
||||
|
||||
@@ -171,7 +171,6 @@ func assertReactionsEqual(t *testing.T, expected, actual []*base.Reaction) {
|
||||
func assertReleaseAssetEqual(t *testing.T, expected, actual *base.ReleaseAsset) {
|
||||
assert.Equal(t, expected.ID, actual.ID)
|
||||
assert.Equal(t, expected.Name, actual.Name)
|
||||
assert.Equal(t, expected.ContentType, actual.ContentType)
|
||||
assert.Equal(t, expected.Size, actual.Size)
|
||||
assert.Equal(t, expected.DownloadCount, actual.DownloadCount)
|
||||
assertTimeEqual(t, expected.Created, actual.Created)
|
||||
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
issues_model "code.gitea.io/gitea/models/issues"
|
||||
"code.gitea.io/gitea/models/organization"
|
||||
"code.gitea.io/gitea/models/perm"
|
||||
access_model "code.gitea.io/gitea/models/perm/access"
|
||||
@@ -62,6 +63,36 @@ func TestTeam_RemoveMember(t *testing.T) {
|
||||
assert.True(t, organization.IsErrLastOrgOwner(err))
|
||||
}
|
||||
|
||||
func TestRemoveTeamMemberRemovesSubscriptionsAndStopwatches(t *testing.T) {
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
|
||||
ctx := t.Context()
|
||||
team := unittest.AssertExistsAndLoadBean(t, &organization.Team{ID: 2})
|
||||
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 4})
|
||||
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 3})
|
||||
issue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{RepoID: repo.ID})
|
||||
|
||||
assert.NoError(t, repo_model.WatchRepo(ctx, user, repo, true))
|
||||
assert.NoError(t, issues_model.CreateOrUpdateIssueWatch(ctx, user.ID, issue.ID, true))
|
||||
ok, err := issues_model.CreateIssueStopwatch(ctx, user, issue)
|
||||
assert.NoError(t, err)
|
||||
assert.True(t, ok)
|
||||
|
||||
assert.NoError(t, RemoveTeamMember(ctx, team, user))
|
||||
|
||||
watch, err := repo_model.GetWatch(ctx, user.ID, repo.ID)
|
||||
assert.NoError(t, err)
|
||||
assert.False(t, repo_model.IsWatchMode(watch.Mode))
|
||||
|
||||
_, exists, err := issues_model.GetIssueWatch(ctx, user.ID, issue.ID)
|
||||
assert.NoError(t, err)
|
||||
assert.False(t, exists)
|
||||
|
||||
hasStopwatch, _, _, err := issues_model.HasUserStopwatch(ctx, user.ID)
|
||||
assert.NoError(t, err)
|
||||
assert.False(t, hasStopwatch)
|
||||
}
|
||||
|
||||
func TestNewTeam(t *testing.T) {
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
|
||||
|
||||
@@ -63,10 +63,10 @@ func NewBlobUploader(ctx context.Context, id string) (*BlobUploader, error) {
|
||||
}
|
||||
|
||||
return &BlobUploader{
|
||||
model,
|
||||
hash,
|
||||
f,
|
||||
false,
|
||||
PackageBlobUpload: model,
|
||||
MultiHasher: hash,
|
||||
file: f,
|
||||
reading: false,
|
||||
}, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -1078,7 +1078,7 @@ func GetPullCommits(ctx context.Context, baseGitRepo *git.Repository, doer *user
|
||||
if pull.HasMerged {
|
||||
baseBranch = pull.MergeBase
|
||||
}
|
||||
prInfo, err := GetCompareInfo(ctx, pull.BaseRepo, pull.BaseRepo, baseGitRepo, baseBranch, pull.GetGitHeadRefName(), true, false)
|
||||
prInfo, err := GetCompareInfo(ctx, pull.BaseRepo, pull.BaseRepo, baseGitRepo, baseBranch, pull.GetGitHeadRefName(), false, false)
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
|
||||
@@ -120,6 +120,11 @@ func ReconsiderWatches(ctx context.Context, repo *repo_model.Repository, user *u
|
||||
return err
|
||||
}
|
||||
|
||||
// Remove all stopwatches a user has running in the repository
|
||||
if err := issues_model.RemoveStopwatchesByRepoID(ctx, user.ID, repo.ID); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Remove all IssueWatches a user has subscribed to in the repository
|
||||
return issues_model.RemoveIssueWatchersByRepoID(ctx, user.ID, repo.ID)
|
||||
}
|
||||
|
||||
@@ -6,7 +6,10 @@ package repository
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"code.gitea.io/gitea/models/db"
|
||||
issues_model "code.gitea.io/gitea/models/issues"
|
||||
"code.gitea.io/gitea/models/perm"
|
||||
access_model "code.gitea.io/gitea/models/perm/access"
|
||||
repo_model "code.gitea.io/gitea/models/repo"
|
||||
"code.gitea.io/gitea/models/unittest"
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
@@ -32,8 +35,8 @@ func TestRepository_AddCollaborator(t *testing.T) {
|
||||
func TestRepository_DeleteCollaboration(t *testing.T) {
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
|
||||
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 4})
|
||||
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 4})
|
||||
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 15})
|
||||
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 22})
|
||||
|
||||
assert.NoError(t, repo.LoadOwner(t.Context()))
|
||||
assert.NoError(t, DeleteCollaboration(t.Context(), repo, user))
|
||||
@@ -44,3 +47,50 @@ func TestRepository_DeleteCollaboration(t *testing.T) {
|
||||
|
||||
unittest.CheckConsistencyFor(t, &repo_model.Repository{ID: repo.ID})
|
||||
}
|
||||
|
||||
func TestRepository_DeleteCollaborationRemovesSubscriptionsAndStopwatches(t *testing.T) {
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
|
||||
ctx := t.Context()
|
||||
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 15})
|
||||
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 22})
|
||||
assert.NoError(t, repo.LoadOwner(ctx))
|
||||
assert.NoError(t, repo_model.WatchRepo(ctx, user, repo, true))
|
||||
|
||||
hasAccess, err := access_model.HasAnyUnitAccess(ctx, user.ID, repo)
|
||||
assert.NoError(t, err)
|
||||
assert.True(t, hasAccess)
|
||||
|
||||
issueCount, err := db.GetEngine(ctx).Where("repo_id=?", repo.ID).Count(new(issues_model.Issue))
|
||||
assert.NoError(t, err)
|
||||
tempIssue := &issues_model.Issue{
|
||||
RepoID: repo.ID,
|
||||
Index: issueCount + 1,
|
||||
PosterID: repo.OwnerID,
|
||||
Title: "temp issue",
|
||||
Content: "temp",
|
||||
}
|
||||
assert.NoError(t, db.Insert(ctx, tempIssue))
|
||||
assert.NoError(t, issues_model.CreateOrUpdateIssueWatch(ctx, user.ID, tempIssue.ID, true))
|
||||
ok, err := issues_model.CreateIssueStopwatch(ctx, user, tempIssue)
|
||||
assert.NoError(t, err)
|
||||
assert.True(t, ok)
|
||||
|
||||
assert.NoError(t, DeleteCollaboration(ctx, repo, user))
|
||||
|
||||
hasAccess, err = access_model.HasAnyUnitAccess(ctx, user.ID, repo)
|
||||
assert.NoError(t, err)
|
||||
assert.False(t, hasAccess)
|
||||
|
||||
watch, err := repo_model.GetWatch(ctx, user.ID, repo.ID)
|
||||
assert.NoError(t, err)
|
||||
assert.False(t, repo_model.IsWatchMode(watch.Mode))
|
||||
|
||||
_, exists, err := issues_model.GetIssueWatch(ctx, user.ID, tempIssue.ID)
|
||||
assert.NoError(t, err)
|
||||
assert.False(t, exists)
|
||||
|
||||
hasStopwatch, _, _, err := issues_model.HasUserStopwatch(ctx, user.ID)
|
||||
assert.NoError(t, err)
|
||||
assert.False(t, hasStopwatch)
|
||||
}
|
||||
|
||||
@@ -197,6 +197,10 @@ func MakeRepoPrivate(ctx context.Context, repo *repo_model.Repository) (err erro
|
||||
return err
|
||||
}
|
||||
|
||||
if err = repo_model.ClearRepoWatches(ctx, repo.ID); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Create/Remove git-daemon-export-ok for git-daemon...
|
||||
if err := CheckDaemonExportOK(ctx, repo); err != nil {
|
||||
return err
|
||||
@@ -220,28 +224,28 @@ func MakeRepoPrivate(ctx context.Context, repo *repo_model.Repository) (err erro
|
||||
})
|
||||
}
|
||||
|
||||
// LinkedRepository returns the linked repo if any
|
||||
func LinkedRepository(ctx context.Context, a *repo_model.Attachment) (*repo_model.Repository, unit.Type, error) {
|
||||
// GetAttachmentLinkedTypeAndRepoID returns the linked type and repository id of attachment if any
|
||||
func GetAttachmentLinkedTypeAndRepoID(ctx context.Context, a *repo_model.Attachment) (unit.Type, int64, error) {
|
||||
if a.IssueID != 0 {
|
||||
iss, err := issues_model.GetIssueByID(ctx, a.IssueID)
|
||||
if err != nil {
|
||||
return nil, unit.TypeIssues, err
|
||||
return unit.TypeIssues, 0, err
|
||||
}
|
||||
repo, err := repo_model.GetRepositoryByID(ctx, iss.RepoID)
|
||||
unitType := unit.TypeIssues
|
||||
if iss.IsPull {
|
||||
unitType = unit.TypePullRequests
|
||||
}
|
||||
return repo, unitType, err
|
||||
} else if a.ReleaseID != 0 {
|
||||
return unitType, iss.RepoID, nil
|
||||
}
|
||||
|
||||
if a.ReleaseID != 0 {
|
||||
rel, err := repo_model.GetReleaseByID(ctx, a.ReleaseID)
|
||||
if err != nil {
|
||||
return nil, unit.TypeReleases, err
|
||||
return unit.TypeReleases, 0, err
|
||||
}
|
||||
repo, err := repo_model.GetRepositoryByID(ctx, rel.RepoID)
|
||||
return repo, unit.TypeReleases, err
|
||||
return unit.TypeReleases, rel.RepoID, nil
|
||||
}
|
||||
return nil, -1, nil
|
||||
return unit.TypeInvalid, 0, nil
|
||||
}
|
||||
|
||||
// CheckDaemonExportOK creates/removes git-daemon-export-ok for git-daemon...
|
||||
|
||||
@@ -13,31 +13,30 @@ import (
|
||||
"code.gitea.io/gitea/models/unittest"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestLinkedRepository(t *testing.T) {
|
||||
func TestAttachLinkedTypeAndRepoID(t *testing.T) {
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
testCases := []struct {
|
||||
name string
|
||||
attachID int64
|
||||
expectedRepo *repo_model.Repository
|
||||
expectedUnitType unit.Type
|
||||
expectedRepoID int64
|
||||
}{
|
||||
{"LinkedIssue", 1, &repo_model.Repository{ID: 1}, unit.TypeIssues},
|
||||
{"LinkedComment", 3, &repo_model.Repository{ID: 1}, unit.TypePullRequests},
|
||||
{"LinkedRelease", 9, &repo_model.Repository{ID: 1}, unit.TypeReleases},
|
||||
{"Notlinked", 10, nil, -1},
|
||||
{"LinkedIssue", 1, unit.TypeIssues, 1},
|
||||
{"LinkedComment", 3, unit.TypePullRequests, 1},
|
||||
{"LinkedRelease", 9, unit.TypeReleases, 1},
|
||||
{"Notlinked", 10, unit.TypeInvalid, 0},
|
||||
}
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
attach, err := repo_model.GetAttachmentByID(t.Context(), tc.attachID)
|
||||
assert.NoError(t, err)
|
||||
repo, unitType, err := LinkedRepository(t.Context(), attach)
|
||||
unitType, repoID, err := GetAttachmentLinkedTypeAndRepoID(t.Context(), attach)
|
||||
assert.NoError(t, err)
|
||||
if tc.expectedRepo != nil {
|
||||
assert.Equal(t, tc.expectedRepo.ID, repo.ID)
|
||||
}
|
||||
assert.Equal(t, tc.expectedUnitType, unitType)
|
||||
assert.Equal(t, tc.expectedRepoID, repoID)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -70,3 +69,24 @@ func TestRepository_HasWiki(t *testing.T) {
|
||||
repo2 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 2})
|
||||
assert.False(t, HasWiki(t.Context(), repo2))
|
||||
}
|
||||
|
||||
func TestMakeRepoPrivateClearsWatches(t *testing.T) {
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
|
||||
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
|
||||
repo.IsPrivate = false
|
||||
|
||||
watchers, err := repo_model.GetRepoWatchersIDs(t.Context(), repo.ID)
|
||||
require.NoError(t, err)
|
||||
require.NotEmpty(t, watchers)
|
||||
|
||||
assert.NoError(t, MakeRepoPrivate(t.Context(), repo))
|
||||
|
||||
watchers, err = repo_model.GetRepoWatchersIDs(t.Context(), repo.ID)
|
||||
assert.NoError(t, err)
|
||||
assert.Empty(t, watchers)
|
||||
|
||||
updatedRepo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: repo.ID})
|
||||
assert.True(t, updatedRepo.IsPrivate)
|
||||
assert.Zero(t, updatedRepo.NumWatches)
|
||||
}
|
||||
|
||||
@@ -241,6 +241,11 @@ func DeleteUser(ctx context.Context, u *user_model.User, purge bool) error {
|
||||
if err := deleteUser(ctx, u, purge); err != nil {
|
||||
return fmt.Errorf("DeleteUser: %w", err)
|
||||
}
|
||||
|
||||
// Finally delete any unlinked attachments, this will also delete the attached files
|
||||
if err := deleteUserUnlinkedAttachments(ctx, u); err != nil {
|
||||
return fmt.Errorf("deleteUserUnlinkedAttachments: %w", err)
|
||||
}
|
||||
return nil
|
||||
}); err != nil {
|
||||
return err
|
||||
@@ -271,6 +276,19 @@ func DeleteUser(ctx context.Context, u *user_model.User, purge bool) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func deleteUserUnlinkedAttachments(ctx context.Context, u *user_model.User) error {
|
||||
attachments, err := repo_model.GetUnlinkedAttachmentsByUserID(ctx, u.ID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("GetUnlinkedAttachmentsByUserID: %w", err)
|
||||
}
|
||||
for _, attach := range attachments {
|
||||
if err := repo_model.DeleteAttachment(ctx, attach, true); err != nil {
|
||||
return fmt.Errorf("DeleteAttachment ID[%d]: %w", attach.ID, err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// DeleteInactiveUsers deletes all inactive users and their email addresses.
|
||||
func DeleteInactiveUsers(ctx context.Context, olderThan time.Duration) error {
|
||||
inactiveUsers, err := user_model.GetInactiveUsers(ctx, olderThan)
|
||||
|
||||
@@ -62,6 +62,24 @@ func TestDeleteUser(t *testing.T) {
|
||||
assert.Error(t, DeleteUser(t.Context(), org, false))
|
||||
}
|
||||
|
||||
func TestDeleteUserUnlinkedAttachments(t *testing.T) {
|
||||
t.Run("DeleteExisting", func(t *testing.T) {
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 8})
|
||||
unittest.AssertExistsAndLoadBean(t, &repo_model.Attachment{ID: 10})
|
||||
|
||||
assert.NoError(t, deleteUserUnlinkedAttachments(t.Context(), user))
|
||||
unittest.AssertNotExistsBean(t, &repo_model.Attachment{ID: 10})
|
||||
})
|
||||
|
||||
t.Run("NoUnlinkedAttachments", func(t *testing.T) {
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
|
||||
|
||||
assert.NoError(t, deleteUserUnlinkedAttachments(t.Context(), user))
|
||||
})
|
||||
}
|
||||
|
||||
func TestPurgeUser(t *testing.T) {
|
||||
test := func(userID int64) {
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
|
||||
@@ -1 +1 @@
|
||||
<a class="author text black tw-font-semibold muted"{{if gt .ID 0}} href="{{.HomeLink}}"{{end}}>{{.GetDisplayName}}</a>{{if .IsTypeBot}}<span class="ui basic label tw-p-1 tw-align-baseline">bot</span>{{end}}
|
||||
<a class="author text black tw-font-semibold muted"{{if gt .ID 0}} href="{{.HomeLink}}"{{end}}>{{.GetDisplayName}}</a>{{if .IsTypeBot}} <span class="ui basic label tw-p-1 tw-align-baseline">bot</span>{{end}}
|
||||
|
||||
@@ -34,6 +34,14 @@ func testGeneratePngBytes() []byte {
|
||||
}
|
||||
|
||||
func testCreateIssueAttachment(t *testing.T, session *TestSession, csrf, repoURL, filename string, content []byte, expectedStatus int) string {
|
||||
return testCreateAttachment(t, session, csrf, repoURL, "issues", filename, content, expectedStatus)
|
||||
}
|
||||
|
||||
func testCreateReleaseAttachment(t *testing.T, session *TestSession, csrf, repoURL, filename string, content []byte, expectedStatus int) string {
|
||||
return testCreateAttachment(t, session, csrf, repoURL, "releases", filename, content, expectedStatus)
|
||||
}
|
||||
|
||||
func testCreateAttachment(t *testing.T, session *TestSession, csrf, repoURL, issueOrRelease, filename string, content []byte, expectedStatus int) string {
|
||||
body := &bytes.Buffer{}
|
||||
|
||||
// Setup multi-part
|
||||
@@ -45,7 +53,7 @@ func testCreateIssueAttachment(t *testing.T, session *TestSession, csrf, repoURL
|
||||
err = writer.Close()
|
||||
assert.NoError(t, err)
|
||||
|
||||
req := NewRequestWithBody(t, "POST", repoURL+"/issues/attachments", body)
|
||||
req := NewRequestWithBody(t, "POST", repoURL+"/"+issueOrRelease+"/attachments", body)
|
||||
req.Header.Add("X-Csrf-Token", csrf)
|
||||
req.Header.Add("Content-Type", writer.FormDataContentType())
|
||||
resp := session.MakeRequest(t, req, expectedStatus)
|
||||
@@ -58,12 +66,25 @@ func testCreateIssueAttachment(t *testing.T, session *TestSession, csrf, repoURL
|
||||
return obj["uuid"]
|
||||
}
|
||||
|
||||
func testDeleteIssueAttachment(t *testing.T, session *TestSession, csrf, repoURL, uuid string, expectedStatus int) {
|
||||
req := NewRequestWithValues(t, "POST", repoURL+"/issues/attachments/remove", map[string]string{"file": uuid})
|
||||
req.Header.Add("X-Csrf-Token", csrf)
|
||||
session.MakeRequest(t, req, expectedStatus)
|
||||
}
|
||||
|
||||
func testDeleteReleaseAttachment(t *testing.T, session *TestSession, csrf, repoURL, uuid string, expectedStatus int) {
|
||||
req := NewRequestWithValues(t, "POST", repoURL+"/releases/attachments/remove", map[string]string{"file": uuid})
|
||||
req.Header.Add("X-Csrf-Token", csrf)
|
||||
session.MakeRequest(t, req, expectedStatus)
|
||||
}
|
||||
|
||||
func TestAttachments(t *testing.T) {
|
||||
defer tests.PrepareTestEnv(t)()
|
||||
t.Run("CreateAnonymousAttachment", testCreateAnonymousAttachment)
|
||||
t.Run("CreateUser2IssueAttachment", testCreateUser2IssueAttachment)
|
||||
t.Run("UploadAttachmentDeleteTemp", testUploadAttachmentDeleteTemp)
|
||||
t.Run("GetAttachment", testGetAttachment)
|
||||
t.Run("DeleteAttachmentPermissions", testDeleteAttachmentPermissions)
|
||||
}
|
||||
|
||||
func testUploadAttachmentDeleteTemp(t *testing.T) {
|
||||
@@ -159,3 +180,31 @@ func testGetAttachment(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func testDeleteAttachmentPermissions(t *testing.T) {
|
||||
const repoURL = "user2/repo1"
|
||||
|
||||
ownerSession := loginUser(t, "user2")
|
||||
readonlySession := loginUser(t, "user5")
|
||||
|
||||
ownerCSRF := GetUserCSRFToken(t, ownerSession)
|
||||
readonlyCSRF := GetUserCSRFToken(t, readonlySession)
|
||||
|
||||
issueFromOwner := testCreateIssueAttachment(t, ownerSession, ownerCSRF, repoURL, "owner-issue.png", testGeneratePngBytes(), http.StatusOK)
|
||||
testDeleteIssueAttachment(t, readonlySession, readonlyCSRF, repoURL, issueFromOwner, http.StatusForbidden)
|
||||
|
||||
issueFromReader := testCreateIssueAttachment(t, readonlySession, readonlyCSRF, repoURL, "reader-issue.png", testGeneratePngBytes(), http.StatusOK)
|
||||
testDeleteIssueAttachment(t, ownerSession, ownerCSRF, repoURL, issueFromReader, http.StatusOK)
|
||||
|
||||
testCreateReleaseAttachment(t, readonlySession, readonlyCSRF, repoURL, "reader-release.png", testGeneratePngBytes(), http.StatusNotFound)
|
||||
|
||||
crossRepoUUID := testCreateIssueAttachment(t, ownerSession, ownerCSRF, repoURL, "cross-repo.png", testGeneratePngBytes(), http.StatusOK)
|
||||
testDeleteIssueAttachment(t, ownerSession, ownerCSRF, "user2/repo2", crossRepoUUID, http.StatusBadRequest)
|
||||
testDeleteIssueAttachment(t, ownerSession, ownerCSRF, repoURL, crossRepoUUID, http.StatusOK)
|
||||
|
||||
releaseUUID := testCreateReleaseAttachment(t, ownerSession, ownerCSRF, repoURL, "reader-release.png", testGeneratePngBytes(), http.StatusOK)
|
||||
testDeleteReleaseAttachment(t, ownerSession, ownerCSRF, repoURL, releaseUUID, http.StatusOK)
|
||||
|
||||
// test deleting release attachment from another repo
|
||||
testDeleteReleaseAttachment(t, ownerSession, ownerCSRF, "user2/repo2", crossRepoUUID, http.StatusBadRequest)
|
||||
}
|
||||
|
||||
@@ -707,6 +707,11 @@ func doAutoPRMerge(baseCtx *APITestContext, dstPath string) func(t *testing.T) {
|
||||
defer tests.PrintCurrentTest(t)()
|
||||
|
||||
ctx := NewAPITestContext(t, baseCtx.Username, baseCtx.Reponame, auth_model.AccessTokenScopeWriteRepository)
|
||||
collaboratorCtx := NewAPITestContext(t, "user5", baseCtx.Reponame, auth_model.AccessTokenScopeWriteRepository)
|
||||
readOnlyCtx := NewAPITestContext(t, "user4", baseCtx.Reponame, auth_model.AccessTokenScopeWriteRepository)
|
||||
|
||||
t.Run("AddAutoMergeCollaborator", doAPIAddCollaborator(*baseCtx, collaboratorCtx.Username, perm.AccessModeWrite))
|
||||
t.Run("AddReadOnlyAutoMergeCollaborator", doAPIAddCollaborator(*baseCtx, readOnlyCtx.Username, perm.AccessModeRead))
|
||||
|
||||
// automerge will merge immediately if the PR is mergeable and there is no "status check" because no status check also means "all checks passed"
|
||||
// so we must set up a status check to test the auto merge feature
|
||||
@@ -738,17 +743,8 @@ func doAutoPRMerge(baseCtx *APITestContext, dstPath string) func(t *testing.T) {
|
||||
|
||||
commitID := path.Base(commitURL)
|
||||
|
||||
addCommitStatus := func(status commitstatus.CommitStatusState) func(*testing.T) {
|
||||
return doAPICreateCommitStatus(ctx, commitID, api.CreateStatusOption{
|
||||
State: status,
|
||||
TargetURL: "http://test.ci/",
|
||||
Description: "",
|
||||
Context: "testci",
|
||||
})
|
||||
}
|
||||
|
||||
// Call API to add Pending status for commit
|
||||
t.Run("CreateStatus", addCommitStatus(commitstatus.CommitStatusPending))
|
||||
t.Run("CreateStatus", doAPICreateCommitStatusTest(ctx, commitID, commitstatus.CommitStatusPending, "testci"))
|
||||
|
||||
// Cancel not existing auto merge
|
||||
ctx.ExpectedCode = http.StatusNotFound
|
||||
@@ -762,10 +758,32 @@ func doAutoPRMerge(baseCtx *APITestContext, dstPath string) func(t *testing.T) {
|
||||
ctx.ExpectedCode = http.StatusConflict
|
||||
t.Run("AutoMergePRTwice", doAPIAutoMergePullRequest(ctx, baseCtx.Username, baseCtx.Reponame, pr.Index))
|
||||
|
||||
// Read-only collaborator still cannot cancel
|
||||
readOnlyCtx.ExpectedCode = http.StatusForbidden
|
||||
t.Run("CancelAutoMergePRByReadOnlyCollaboratorForbidden", doAPICancelAutoMergePullRequest(readOnlyCtx, baseCtx.Username, baseCtx.Reponame, pr.Index))
|
||||
readOnlyCtx.ExpectedCode = 0
|
||||
|
||||
// Collaborators with merge permissions can cancel a schedule made by someone else
|
||||
collaboratorCtx.ExpectedCode = http.StatusNoContent
|
||||
t.Run("CancelAutoMergePRByCollaborator", doAPICancelAutoMergePullRequest(collaboratorCtx, baseCtx.Username, baseCtx.Reponame, pr.Index))
|
||||
collaboratorCtx.ExpectedCode = 0
|
||||
|
||||
// Re-add auto merge request so the repo owner can cancel it as well
|
||||
ctx.ExpectedCode = http.StatusCreated
|
||||
t.Run("AutoMergePRAfterCollaboratorCancel", doAPIAutoMergePullRequest(ctx, baseCtx.Username, baseCtx.Reponame, pr.Index))
|
||||
|
||||
// Cancel auto merge request
|
||||
ctx.ExpectedCode = http.StatusNoContent
|
||||
t.Run("CancelAutoMergePR", doAPICancelAutoMergePullRequest(ctx, baseCtx.Username, baseCtx.Reponame, pr.Index))
|
||||
|
||||
// Collaborator can schedule but admins should still be able to cancel their schedule
|
||||
collaboratorCtx.ExpectedCode = http.StatusCreated
|
||||
t.Run("AutoMergePRByCollaborator", doAPIAutoMergePullRequest(collaboratorCtx, baseCtx.Username, baseCtx.Reponame, pr.Index))
|
||||
collaboratorCtx.ExpectedCode = 0
|
||||
|
||||
ctx.ExpectedCode = http.StatusNoContent
|
||||
t.Run("CancelAutoMergePRByAdmin", doAPICancelAutoMergePullRequest(ctx, baseCtx.Username, baseCtx.Reponame, pr.Index))
|
||||
|
||||
// Add auto merge request
|
||||
ctx.ExpectedCode = http.StatusCreated
|
||||
t.Run("AutoMergePR", doAPIAutoMergePullRequest(ctx, baseCtx.Username, baseCtx.Reponame, pr.Index))
|
||||
@@ -777,7 +795,7 @@ func doAutoPRMerge(baseCtx *APITestContext, dstPath string) func(t *testing.T) {
|
||||
assert.False(t, pr.HasMerged)
|
||||
|
||||
// Call API to add Failure status for commit
|
||||
t.Run("CreateStatus", addCommitStatus(commitstatus.CommitStatusFailure))
|
||||
t.Run("CreateStatus", doAPICreateCommitStatusTest(ctx, commitID, commitstatus.CommitStatusFailure, "testci"))
|
||||
|
||||
// Check pr status
|
||||
pr, err = doAPIGetPullRequest(ctx, baseCtx.Username, baseCtx.Reponame, pr.Index)(t)
|
||||
@@ -785,8 +803,7 @@ func doAutoPRMerge(baseCtx *APITestContext, dstPath string) func(t *testing.T) {
|
||||
assert.False(t, pr.HasMerged)
|
||||
|
||||
// Call API to add Success status for commit
|
||||
t.Run("CreateStatus", addCommitStatus(commitstatus.CommitStatusSuccess))
|
||||
|
||||
t.Run("CreateStatus", doAPICreateCommitStatusTest(ctx, commitID, commitstatus.CommitStatusSuccess, "testci"))
|
||||
// wait to let gitea merge stuff
|
||||
time.Sleep(time.Second)
|
||||
|
||||
|
||||
@@ -338,7 +338,7 @@ func NewRequestWithBody(t testing.TB, method, urlStr string, body io.Reader) *Re
|
||||
return &RequestWrapper{req}
|
||||
}
|
||||
|
||||
const NoExpectedStatus = -1
|
||||
const NoExpectedStatus = 0
|
||||
|
||||
func MakeRequest(t testing.TB, rw *RequestWrapper, expectedStatus int) *httptest.ResponseRecorder {
|
||||
t.Helper()
|
||||
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
@@ -95,6 +96,45 @@ func TestAuthorizeShow(t *testing.T) {
|
||||
htmlDoc.GetCSRF()
|
||||
}
|
||||
|
||||
func TestAuthorizeGrantS256RequiresVerifier(t *testing.T) {
|
||||
defer tests.PrepareTestEnv(t)()
|
||||
ctx := loginUser(t, "user4")
|
||||
codeChallenge := "CjvyTLSdR47G5zYenDA-eDWW4lRrO8yvjcWwbD_deOg"
|
||||
req := NewRequest(t, "GET", "/login/oauth/authorize?client_id=da7da3ba-9a13-4167-856f-3899de0b0138&redirect_uri=a&response_type=code&state=thestate&code_challenge_method=S256&code_challenge="+url.QueryEscape(codeChallenge))
|
||||
resp := ctx.MakeRequest(t, req, http.StatusOK)
|
||||
|
||||
htmlDoc := NewHTMLParser(t, resp.Body)
|
||||
AssertHTMLElement(t, htmlDoc, "#authorize-app", true)
|
||||
|
||||
grantReq := NewRequestWithValues(t, "POST", "/login/oauth/grant", map[string]string{
|
||||
"client_id": "da7da3ba-9a13-4167-856f-3899de0b0138",
|
||||
"state": "thestate",
|
||||
"scope": "",
|
||||
"nonce": "",
|
||||
"redirect_uri": "a",
|
||||
"granted": "true",
|
||||
"_csrf": htmlDoc.GetCSRF(),
|
||||
})
|
||||
grantResp := ctx.MakeRequest(t, grantReq, http.StatusSeeOther)
|
||||
u, err := grantResp.Result().Location()
|
||||
assert.NoError(t, err)
|
||||
code := u.Query().Get("code")
|
||||
assert.NotEmpty(t, code)
|
||||
|
||||
accessReq := NewRequestWithValues(t, "POST", "/login/oauth/access_token", map[string]string{
|
||||
"grant_type": "authorization_code",
|
||||
"client_id": "da7da3ba-9a13-4167-856f-3899de0b0138",
|
||||
"client_secret": "4MK8Na6R55smdCY0WuCCumZ6hjRPnGY5saWVRHHjJiA=",
|
||||
"redirect_uri": "a",
|
||||
"code": code,
|
||||
})
|
||||
accessResp := MakeRequest(t, accessReq, http.StatusBadRequest)
|
||||
parsedError := new(oauth2_provider.AccessTokenError)
|
||||
assert.NoError(t, json.Unmarshal(accessResp.Body.Bytes(), parsedError))
|
||||
assert.Equal(t, "unauthorized_client", string(parsedError.ErrorCode))
|
||||
assert.Equal(t, "failed PKCE code challenge", parsedError.ErrorDescription)
|
||||
}
|
||||
|
||||
func TestAuthorizeRedirectWithExistingGrant(t *testing.T) {
|
||||
defer tests.PrepareTestEnv(t)()
|
||||
req := NewRequest(t, "GET", "/login/oauth/authorize?client_id=da7da3ba-9a13-4167-856f-3899de0b0138&redirect_uri=https%3A%2F%2Fexample.com%2Fxyzzy&response_type=code&state=thestate")
|
||||
|
||||
@@ -77,12 +77,7 @@ func TestPullCreate_CommitStatus(t *testing.T) {
|
||||
// Update commit status, and check if icon is updated as well
|
||||
for _, status := range statusList {
|
||||
// Call API to add status for commit
|
||||
t.Run("CreateStatus", doAPICreateCommitStatus(testCtx, commitID, api.CreateStatusOption{
|
||||
State: status,
|
||||
TargetURL: "http://test.ci/",
|
||||
Description: "",
|
||||
Context: "testci",
|
||||
}))
|
||||
t.Run("CreateStatus", doAPICreateCommitStatusTest(testCtx, commitID, status, "testci"))
|
||||
|
||||
req = NewRequest(t, "GET", "/user1/repo1/pulls/1/commits")
|
||||
resp = session.MakeRequest(t, req, http.StatusOK)
|
||||
@@ -104,19 +99,15 @@ func TestPullCreate_CommitStatus(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
func doAPICreateCommitStatus(ctx APITestContext, commitID string, data api.CreateStatusOption) func(*testing.T) {
|
||||
func doAPICreateCommitStatusTest(ctx APITestContext, ref string, state commitstatus.CommitStatusState, statusContext string) func(*testing.T) {
|
||||
return func(t *testing.T) {
|
||||
req := NewRequestWithJSON(
|
||||
t,
|
||||
http.MethodPost,
|
||||
fmt.Sprintf("/api/v1/repos/%s/%s/statuses/%s", ctx.Username, ctx.Reponame, commitID),
|
||||
data,
|
||||
).AddTokenAuth(ctx.Token)
|
||||
if ctx.ExpectedCode != 0 {
|
||||
ctx.Session.MakeRequest(t, req, ctx.ExpectedCode)
|
||||
return
|
||||
}
|
||||
ctx.Session.MakeRequest(t, req, http.StatusCreated)
|
||||
link := fmt.Sprintf("/api/v1/repos/%s/%s/statuses/%s", ctx.Username, ctx.Reponame, url.PathEscape(ref))
|
||||
req := NewRequestWithJSON(t, http.MethodPost, link, api.CreateStatusOption{
|
||||
State: state,
|
||||
TargetURL: "http://test.ci/",
|
||||
Context: statusContext,
|
||||
}).AddTokenAuth(ctx.Token)
|
||||
ctx.Session.MakeRequest(t, req, ctx.ExpectedCode)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -6,12 +6,13 @@ package integration
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"path"
|
||||
"sync"
|
||||
"testing"
|
||||
|
||||
auth_model "code.gitea.io/gitea/models/auth"
|
||||
"code.gitea.io/gitea/models/db"
|
||||
git_model "code.gitea.io/gitea/models/git"
|
||||
"code.gitea.io/gitea/modules/commitstatus"
|
||||
"code.gitea.io/gitea/modules/json"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
@@ -20,6 +21,7 @@ import (
|
||||
|
||||
"github.com/PuerkitoBio/goquery"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestRepoCommits(t *testing.T) {
|
||||
@@ -79,98 +81,87 @@ func TestRepoCommits(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
func doTestRepoCommitWithStatus(t *testing.T, state string, classes ...string) {
|
||||
func TestRepoCommitsWithStatus(t *testing.T) {
|
||||
defer tests.PrepareTestEnv(t)()
|
||||
|
||||
session := loginUser(t, "user2")
|
||||
|
||||
// Request repository commits page
|
||||
req := NewRequest(t, "GET", "/user2/repo1/commits/branch/master")
|
||||
resp := session.MakeRequest(t, req, http.StatusOK)
|
||||
|
||||
doc := NewHTMLParser(t, resp.Body)
|
||||
// Get first commit URL
|
||||
commitURL, exists := doc.doc.Find("#commits-table .commit-id-short").Attr("href")
|
||||
assert.True(t, exists)
|
||||
assert.NotEmpty(t, commitURL)
|
||||
|
||||
// Call API to add status for commit
|
||||
ctx := NewAPITestContext(t, "user2", "repo1", auth_model.AccessTokenScopeWriteRepository)
|
||||
t.Run("CreateStatus", doAPICreateCommitStatus(ctx, path.Base(commitURL), api.CreateStatusOption{
|
||||
State: commitstatus.CommitStatusState(state),
|
||||
TargetURL: "http://test.ci/",
|
||||
Description: "",
|
||||
Context: "testci",
|
||||
}))
|
||||
|
||||
req = NewRequest(t, "GET", "/user2/repo1/commits/branch/master")
|
||||
resp = session.MakeRequest(t, req, http.StatusOK)
|
||||
|
||||
doc = NewHTMLParser(t, resp.Body)
|
||||
// Check if commit status is displayed in message column (.tippy-target to ignore the tippy trigger)
|
||||
sel := doc.doc.Find("#commits-table .message .tippy-target .commit-status")
|
||||
assert.Equal(t, 1, sel.Length())
|
||||
for _, class := range classes {
|
||||
assert.True(t, sel.HasClass(class))
|
||||
requestCommitStatuses := func(t *testing.T, linkList, linkCombined string) (statuses []*api.CommitStatus, status api.CombinedStatus) {
|
||||
assert.NoError(t, json.Unmarshal(session.MakeRequest(t, NewRequest(t, "GET", linkList), http.StatusOK).Body.Bytes(), &statuses))
|
||||
assert.NoError(t, json.Unmarshal(session.MakeRequest(t, NewRequest(t, "GET", linkCombined), http.StatusOK).Body.Bytes(), &status))
|
||||
return statuses, status
|
||||
}
|
||||
|
||||
// By SHA
|
||||
req = NewRequest(t, "GET", "/api/v1/repos/user2/repo1/commits/"+path.Base(commitURL)+"/statuses")
|
||||
reqOne := NewRequest(t, "GET", "/api/v1/repos/user2/repo1/commits/"+path.Base(commitURL)+"/status")
|
||||
testRepoCommitsWithStatus(t, session.MakeRequest(t, req, http.StatusOK), session.MakeRequest(t, reqOne, http.StatusOK), state)
|
||||
testRefMaster := func(t *testing.T, state commitstatus.CommitStatusState, classes ...string) {
|
||||
_ = db.TruncateBeans(t.Context(), &git_model.CommitStatus{})
|
||||
|
||||
// By short SHA
|
||||
req = NewRequest(t, "GET", "/api/v1/repos/user2/repo1/commits/"+path.Base(commitURL)[:10]+"/statuses")
|
||||
reqOne = NewRequest(t, "GET", "/api/v1/repos/user2/repo1/commits/"+path.Base(commitURL)[:10]+"/status")
|
||||
testRepoCommitsWithStatus(t, session.MakeRequest(t, req, http.StatusOK), session.MakeRequest(t, reqOne, http.StatusOK), state)
|
||||
// Request repository commits page
|
||||
req := NewRequest(t, "GET", "/user2/repo1/commits/branch/master")
|
||||
resp := session.MakeRequest(t, req, http.StatusOK)
|
||||
|
||||
// By Ref
|
||||
req = NewRequest(t, "GET", "/api/v1/repos/user2/repo1/commits/master/statuses")
|
||||
reqOne = NewRequest(t, "GET", "/api/v1/repos/user2/repo1/commits/master/status")
|
||||
testRepoCommitsWithStatus(t, session.MakeRequest(t, req, http.StatusOK), session.MakeRequest(t, reqOne, http.StatusOK), state)
|
||||
req = NewRequest(t, "GET", "/api/v1/repos/user2/repo1/commits/v1.1/statuses")
|
||||
reqOne = NewRequest(t, "GET", "/api/v1/repos/user2/repo1/commits/v1.1/status")
|
||||
testRepoCommitsWithStatus(t, session.MakeRequest(t, req, http.StatusOK), session.MakeRequest(t, reqOne, http.StatusOK), state)
|
||||
}
|
||||
doc := NewHTMLParser(t, resp.Body)
|
||||
// Get first commit URL
|
||||
commitURL, _ := doc.doc.Find("#commits-table .commit-id-short").Attr("href")
|
||||
require.NotEmpty(t, commitURL)
|
||||
commitID := path.Base(commitURL)
|
||||
|
||||
func testRepoCommitsWithStatus(t *testing.T, resp, respOne *httptest.ResponseRecorder, state string) {
|
||||
var statuses []*api.CommitStatus
|
||||
assert.NoError(t, json.Unmarshal(resp.Body.Bytes(), &statuses))
|
||||
var status api.CombinedStatus
|
||||
assert.NoError(t, json.Unmarshal(respOne.Body.Bytes(), &status))
|
||||
assert.NotNil(t, status)
|
||||
// Call API to add status for commit
|
||||
doAPICreateCommitStatusTest(ctx, path.Base(commitURL), state, "testci")(t)
|
||||
|
||||
if assert.Len(t, statuses, 1) {
|
||||
assert.Equal(t, commitstatus.CommitStatusState(state), statuses[0].State)
|
||||
assert.Equal(t, setting.AppURL+"api/v1/repos/user2/repo1/statuses/65f1bf27bc3bf70f64657658635e66094edbcb4d", statuses[0].URL)
|
||||
assert.Equal(t, "http://test.ci/", statuses[0].TargetURL)
|
||||
assert.Empty(t, statuses[0].Description)
|
||||
assert.Equal(t, "testci", statuses[0].Context)
|
||||
req = NewRequest(t, "GET", "/user2/repo1/commits/branch/master")
|
||||
resp = session.MakeRequest(t, req, http.StatusOK)
|
||||
|
||||
assert.Len(t, status.Statuses, 1)
|
||||
assert.Equal(t, statuses[0], status.Statuses[0])
|
||||
assert.Equal(t, "65f1bf27bc3bf70f64657658635e66094edbcb4d", status.SHA)
|
||||
doc = NewHTMLParser(t, resp.Body)
|
||||
// Check if commit status is displayed in message column (.tippy-target to ignore the tippy trigger)
|
||||
sel := doc.doc.Find("#commits-table .message .tippy-target .commit-status")
|
||||
assert.Equal(t, 1, sel.Length())
|
||||
for _, class := range classes {
|
||||
assert.True(t, sel.HasClass(class))
|
||||
}
|
||||
|
||||
testRepoCommitsWithStatus := func(t *testing.T, linkList, linkCombined string, state commitstatus.CommitStatusState) {
|
||||
statuses, status := requestCommitStatuses(t, linkList, linkCombined)
|
||||
require.Len(t, statuses, 1)
|
||||
require.NotNil(t, status)
|
||||
|
||||
assert.Equal(t, state, statuses[0].State)
|
||||
assert.Equal(t, setting.AppURL+"api/v1/repos/user2/repo1/statuses/"+commitID, statuses[0].URL)
|
||||
assert.Equal(t, "http://test.ci/", statuses[0].TargetURL)
|
||||
assert.Empty(t, statuses[0].Description)
|
||||
assert.Equal(t, "testci", statuses[0].Context)
|
||||
|
||||
assert.Len(t, status.Statuses, 1)
|
||||
assert.Equal(t, statuses[0], status.Statuses[0])
|
||||
assert.Equal(t, commitID, status.SHA)
|
||||
}
|
||||
// By SHA
|
||||
testRepoCommitsWithStatus(t, "/api/v1/repos/user2/repo1/commits/"+commitID+"/statuses", "/api/v1/repos/user2/repo1/commits/"+commitID+"/status", state)
|
||||
// By short SHA
|
||||
testRepoCommitsWithStatus(t, "/api/v1/repos/user2/repo1/commits/"+commitID[:7]+"/statuses", "/api/v1/repos/user2/repo1/commits/"+commitID[:7]+"/status", state)
|
||||
// By Ref
|
||||
testRepoCommitsWithStatus(t, "/api/v1/repos/user2/repo1/commits/master/statuses", "/api/v1/repos/user2/repo1/commits/master/status", state)
|
||||
// Tag "v1.1" points to master
|
||||
testRepoCommitsWithStatus(t, "/api/v1/repos/user2/repo1/commits/v1.1/statuses", "/api/v1/repos/user2/repo1/commits/v1.1/status", state)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRepoCommitsWithStatusPending(t *testing.T) {
|
||||
doTestRepoCommitWithStatus(t, "pending", "octicon-dot-fill", "yellow")
|
||||
}
|
||||
t.Run("pending", func(t *testing.T) { testRefMaster(t, "pending", "octicon-dot-fill", "yellow") })
|
||||
t.Run("success", func(t *testing.T) { testRefMaster(t, "success", "octicon-check", "green") })
|
||||
t.Run("error", func(t *testing.T) { testRefMaster(t, "error", "gitea-exclamation", "red") })
|
||||
t.Run("failure", func(t *testing.T) { testRefMaster(t, "failure", "octicon-x", "red") })
|
||||
t.Run("warning", func(t *testing.T) { testRefMaster(t, "warning", "gitea-exclamation", "yellow") })
|
||||
t.Run("BranchWithSlash", func(t *testing.T) {
|
||||
_ = db.TruncateBeans(t.Context(), &git_model.CommitStatus{})
|
||||
|
||||
func TestRepoCommitsWithStatusSuccess(t *testing.T) {
|
||||
doTestRepoCommitWithStatus(t, "success", "octicon-check", "green")
|
||||
}
|
||||
|
||||
func TestRepoCommitsWithStatusError(t *testing.T) {
|
||||
doTestRepoCommitWithStatus(t, "error", "gitea-exclamation", "red")
|
||||
}
|
||||
|
||||
func TestRepoCommitsWithStatusFailure(t *testing.T) {
|
||||
doTestRepoCommitWithStatus(t, "failure", "octicon-x", "red")
|
||||
}
|
||||
|
||||
func TestRepoCommitsWithStatusWarning(t *testing.T) {
|
||||
doTestRepoCommitWithStatus(t, "warning", "gitea-exclamation", "yellow")
|
||||
linkList, linkCombined := "/api/v1/repos/user2/repo1/commits/feature%2F1/statuses", "/api/v1/repos/user2/repo1/commits/feature/1/status"
|
||||
statuses, status := requestCommitStatuses(t, linkList, linkCombined)
|
||||
assert.Empty(t, statuses)
|
||||
assert.Empty(t, status.Statuses)
|
||||
doAPICreateCommitStatusTest(ctx, "feature/1", commitstatus.CommitStatusSuccess, "testci")(t)
|
||||
statuses, status = requestCommitStatuses(t, linkList, linkCombined)
|
||||
assert.NotEmpty(t, statuses)
|
||||
assert.NotEmpty(t, status.Statuses)
|
||||
})
|
||||
}
|
||||
|
||||
func TestRepoCommitsStatusParallel(t *testing.T) {
|
||||
@@ -194,13 +185,7 @@ func TestRepoCommitsStatusParallel(t *testing.T) {
|
||||
go func(parentT *testing.T, i int) {
|
||||
parentT.Run(fmt.Sprintf("ParallelCreateStatus_%d", i), func(t *testing.T) {
|
||||
ctx := NewAPITestContext(t, "user2", "repo1", auth_model.AccessTokenScopeWriteRepository)
|
||||
runBody := doAPICreateCommitStatus(ctx, path.Base(commitURL), api.CreateStatusOption{
|
||||
State: commitstatus.CommitStatusPending,
|
||||
TargetURL: "http://test.ci/",
|
||||
Description: "",
|
||||
Context: "testci",
|
||||
})
|
||||
runBody(t)
|
||||
doAPICreateCommitStatusTest(ctx, path.Base(commitURL), commitstatus.CommitStatusPending, "testci")(t)
|
||||
wg.Done()
|
||||
})
|
||||
}(t, i)
|
||||
@@ -225,20 +210,8 @@ func TestRepoCommitsStatusMultiple(t *testing.T) {
|
||||
|
||||
// Call API to add status for commit
|
||||
ctx := NewAPITestContext(t, "user2", "repo1", auth_model.AccessTokenScopeWriteRepository)
|
||||
t.Run("CreateStatus", doAPICreateCommitStatus(ctx, path.Base(commitURL), api.CreateStatusOption{
|
||||
State: commitstatus.CommitStatusSuccess,
|
||||
TargetURL: "http://test.ci/",
|
||||
Description: "",
|
||||
Context: "testci",
|
||||
}))
|
||||
|
||||
t.Run("CreateStatus", doAPICreateCommitStatus(ctx, path.Base(commitURL), api.CreateStatusOption{
|
||||
State: commitstatus.CommitStatusSuccess,
|
||||
TargetURL: "http://test.ci/",
|
||||
Description: "",
|
||||
Context: "other_context",
|
||||
}))
|
||||
|
||||
t.Run("CreateStatus", doAPICreateCommitStatusTest(ctx, path.Base(commitURL), commitstatus.CommitStatusSuccess, "testci"))
|
||||
t.Run("CreateStatus", doAPICreateCommitStatusTest(ctx, path.Base(commitURL), commitstatus.CommitStatusSuccess, "other_context"))
|
||||
req = NewRequest(t, "GET", "/user2/repo1/commits/branch/master")
|
||||
resp = session.MakeRequest(t, req, http.StatusOK)
|
||||
|
||||
|
||||
@@ -934,12 +934,7 @@ func Test_WebhookStatus(t *testing.T) {
|
||||
testCtx := NewAPITestContext(t, "user2", "repo1", auth_model.AccessTokenScopeAll)
|
||||
|
||||
// update a status for a commit via API
|
||||
doAPICreateCommitStatus(testCtx, commitID, api.CreateStatusOption{
|
||||
State: commitstatus.CommitStatusSuccess,
|
||||
TargetURL: "http://test.ci/",
|
||||
Description: "",
|
||||
Context: "testci",
|
||||
})(t)
|
||||
doAPICreateCommitStatusTest(testCtx, commitID, commitstatus.CommitStatusSuccess, "testci")(t)
|
||||
|
||||
// 3. validate the webhook is triggered
|
||||
assert.Equal(t, "status", triggeredEvent)
|
||||
|
||||
@@ -35,7 +35,7 @@ const baseOptions: MonacoOpts = {
|
||||
renderLineHighlight: 'all',
|
||||
renderLineHighlightOnlyWhenFocus: true,
|
||||
rulers: [],
|
||||
scrollbar: {horizontalScrollbarSize: 6, verticalScrollbarSize: 6},
|
||||
scrollbar: {horizontalScrollbarSize: 6, verticalScrollbarSize: 6, alwaysConsumeMouseWheel: false},
|
||||
scrollBeyondLastLine: false,
|
||||
automaticLayout: true,
|
||||
};
|
||||
|
||||
@@ -176,6 +176,7 @@ export function markdownHandleIndention(tvs: TextareaValueSelection): MarkdownHa
|
||||
}
|
||||
|
||||
function handleNewline(textarea: HTMLTextAreaElement, e: Event) {
|
||||
if ((e as KeyboardEvent).isComposing) return;
|
||||
const ret = markdownHandleIndention({value: textarea.value, selStart: textarea.selectionStart, selEnd: textarea.selectionEnd});
|
||||
if (!ret.handled) return;
|
||||
e.preventDefault();
|
||||
|
||||
@@ -4,6 +4,9 @@ import {GET, POST} from '../modules/fetch.ts';
|
||||
|
||||
const {appSubUrl} = window.config;
|
||||
|
||||
/** One of the possible values for the `data-webauthn-error-msg` attribute on the webauthn error message element */
|
||||
type ErrorType = 'general' | 'insecure' | 'browser' | 'unable-to-process' | 'duplicated' | 'unknown';
|
||||
|
||||
export async function initUserAuthWebAuthn() {
|
||||
const elPrompt = document.querySelector('.user.signin.webauthn-prompt');
|
||||
const elSignInPasskeyBtn = document.querySelector('.signin-passkey');
|
||||
@@ -11,7 +14,8 @@ export async function initUserAuthWebAuthn() {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!detectWebAuthnSupport()) {
|
||||
const errorType = detectWebAuthnSupport();
|
||||
if (errorType) {
|
||||
if (elSignInPasskeyBtn) hideElem(elSignInPasskeyBtn);
|
||||
return;
|
||||
}
|
||||
@@ -177,7 +181,7 @@ async function webauthnRegistered(newCredential: any) { // TODO: Credential type
|
||||
window.location.reload();
|
||||
}
|
||||
|
||||
function webAuthnError(errorType: string, message:string = '') {
|
||||
function webAuthnError(errorType: ErrorType, message:string = '') {
|
||||
const elErrorMsg = document.querySelector(`#webauthn-error-msg`);
|
||||
|
||||
if (errorType === 'general') {
|
||||
@@ -194,25 +198,26 @@ function webAuthnError(errorType: string, message:string = '') {
|
||||
showElem('#webauthn-error');
|
||||
}
|
||||
|
||||
function detectWebAuthnSupport() {
|
||||
/** Returns the error type or `null` when there was no error. */
|
||||
function detectWebAuthnSupport(): ErrorType | null {
|
||||
if (!window.isSecureContext) {
|
||||
webAuthnError('insecure');
|
||||
return false;
|
||||
return 'insecure';
|
||||
}
|
||||
|
||||
if (typeof window.PublicKeyCredential !== 'function') {
|
||||
webAuthnError('browser');
|
||||
return false;
|
||||
return 'browser';
|
||||
}
|
||||
|
||||
return true;
|
||||
return null;
|
||||
}
|
||||
|
||||
export function initUserAuthWebAuthnRegister() {
|
||||
const elRegister = document.querySelector<HTMLInputElement>('#register-webauthn');
|
||||
if (!elRegister) return;
|
||||
|
||||
if (!detectWebAuthnSupport()) {
|
||||
const errorType = detectWebAuthnSupport();
|
||||
if (errorType) {
|
||||
webAuthnError(errorType);
|
||||
elRegister.disabled = true;
|
||||
return;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user