mirror of
https://github.com/go-gitea/gitea.git
synced 2026-10-22 21:13:47 +02:00
Merge branch 'main' into fix-24635
Signed-off-by: Excellencedev <ademiluyisuccessandexcellence@gmail.com>
This commit is contained in:
110 files changed
+2702
-1720
No files matched your search
@@ -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,
|
||||
|
||||
@@ -166,16 +166,6 @@ func parseGitDiffTreeLine(line string) (*DiffTreeRecord, error) {
|
||||
return nil, fmt.Errorf("unparsable output for diff-tree --raw: `%s`, expected 5 space delimited values got %d)", line, len(fields))
|
||||
}
|
||||
|
||||
baseMode, err := git.ParseEntryMode(fields[0])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
headMode, err := git.ParseEntryMode(fields[1])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
baseBlobID := fields[2]
|
||||
headBlobID := fields[3]
|
||||
|
||||
@@ -201,8 +191,8 @@ func parseGitDiffTreeLine(line string) (*DiffTreeRecord, error) {
|
||||
return &DiffTreeRecord{
|
||||
Status: status,
|
||||
Score: score,
|
||||
BaseMode: baseMode,
|
||||
HeadMode: headMode,
|
||||
BaseMode: git.ParseEntryMode(fields[0]),
|
||||
HeadMode: git.ParseEntryMode(fields[1]),
|
||||
BaseBlobID: baseBlobID,
|
||||
HeadBlobID: headBlobID,
|
||||
BasePath: basePath,
|
||||
|
||||
+46
-31
@@ -399,20 +399,20 @@ type DiffFile struct {
|
||||
isAmbiguous bool
|
||||
|
||||
// basic fields (parsed from diff result)
|
||||
Name string
|
||||
NameHash string
|
||||
OldName string
|
||||
Addition int
|
||||
Deletion int
|
||||
Type DiffFileType
|
||||
Mode string
|
||||
OldMode string
|
||||
IsCreated bool
|
||||
IsDeleted bool
|
||||
IsBin bool
|
||||
IsLFSFile bool
|
||||
IsRenamed bool
|
||||
IsSubmodule bool
|
||||
Name string
|
||||
NameHash string
|
||||
OldName string
|
||||
Addition int
|
||||
Deletion int
|
||||
Type DiffFileType
|
||||
EntryMode string
|
||||
OldEntryMode string
|
||||
IsCreated bool
|
||||
IsDeleted bool
|
||||
IsBin bool
|
||||
IsLFSFile bool
|
||||
IsRenamed bool
|
||||
IsSubmodule bool
|
||||
// basic fields but for render purpose only
|
||||
Sections []*DiffSection
|
||||
IsIncomplete bool
|
||||
@@ -501,21 +501,36 @@ func (diffFile *DiffFile) ShouldBeHidden() bool {
|
||||
return diffFile.IsGenerated || diffFile.IsViewed
|
||||
}
|
||||
|
||||
func (diffFile *DiffFile) ModeTranslationKey(mode string) string {
|
||||
switch mode {
|
||||
case "040000":
|
||||
return "git.filemode.directory"
|
||||
case "100644":
|
||||
return "git.filemode.normal_file"
|
||||
case "100755":
|
||||
return "git.filemode.executable_file"
|
||||
case "120000":
|
||||
return "git.filemode.symbolic_link"
|
||||
case "160000":
|
||||
return "git.filemode.submodule"
|
||||
default:
|
||||
return mode
|
||||
func (diffFile *DiffFile) TranslateDiffEntryMode(locale translation.Locale) string {
|
||||
entryModeTr := func(mode string) string {
|
||||
entryMode := git.ParseEntryMode(mode)
|
||||
switch {
|
||||
case entryMode.IsDir():
|
||||
return locale.TrString("git.filemode.directory")
|
||||
case entryMode.IsRegular():
|
||||
return locale.TrString("git.filemode.normal_file")
|
||||
case entryMode.IsExecutable():
|
||||
return locale.TrString("git.filemode.executable_file")
|
||||
case entryMode.IsLink():
|
||||
return locale.TrString("git.filemode.symbolic_link")
|
||||
case entryMode.IsSubModule():
|
||||
return locale.TrString("git.filemode.submodule")
|
||||
default:
|
||||
return mode
|
||||
}
|
||||
}
|
||||
|
||||
if diffFile.EntryMode != "" && diffFile.OldEntryMode != "" {
|
||||
oldMode := entryModeTr(diffFile.OldEntryMode)
|
||||
newMode := entryModeTr(diffFile.EntryMode)
|
||||
return locale.TrString("git.filemode.changed_filemode", oldMode, newMode)
|
||||
}
|
||||
if diffFile.EntryMode != "" {
|
||||
if entryMode := git.ParseEntryMode(diffFile.EntryMode); !entryMode.IsRegular() {
|
||||
return entryModeTr(diffFile.EntryMode)
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
type limitByteWriter struct {
|
||||
@@ -695,10 +710,10 @@ parsingLoop:
|
||||
strings.HasPrefix(line, "new mode "):
|
||||
|
||||
if strings.HasPrefix(line, "old mode ") {
|
||||
curFile.OldMode = prepareValue(line, "old mode ")
|
||||
curFile.OldEntryMode = prepareValue(line, "old mode ")
|
||||
}
|
||||
if strings.HasPrefix(line, "new mode ") {
|
||||
curFile.Mode = prepareValue(line, "new mode ")
|
||||
curFile.EntryMode = prepareValue(line, "new mode ")
|
||||
}
|
||||
if strings.HasSuffix(line, " 160000\n") {
|
||||
curFile.IsSubmodule, curFile.SubmoduleDiffInfo = true, &SubmoduleDiffInfo{}
|
||||
@@ -733,7 +748,7 @@ parsingLoop:
|
||||
curFile.Type = DiffFileAdd
|
||||
curFile.IsCreated = true
|
||||
if strings.HasPrefix(line, "new file mode ") {
|
||||
curFile.Mode = prepareValue(line, "new file mode ")
|
||||
curFile.EntryMode = prepareValue(line, "new file mode ")
|
||||
}
|
||||
if strings.HasSuffix(line, " 160000\n") {
|
||||
curFile.IsSubmodule, curFile.SubmoduleDiffInfo = true, &SubmoduleDiffInfo{}
|
||||
|
||||
+12
-21
@@ -11,7 +11,6 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"maps"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"regexp"
|
||||
@@ -487,40 +486,32 @@ func buildObjectResponse(rc *requestContext, pointer lfs_module.Pointer, downloa
|
||||
rep.Error = err
|
||||
} else {
|
||||
rep.Actions = make(map[string]*lfs_module.Link)
|
||||
|
||||
header := make(map[string]string)
|
||||
|
||||
if len(rc.Authorization) > 0 {
|
||||
header["Authorization"] = rc.Authorization
|
||||
}
|
||||
|
||||
if download {
|
||||
var link *lfs_module.Link
|
||||
if setting.LFS.Storage.ServeDirect() {
|
||||
// If we have a signed url (S3, object storage), redirect to this directly.
|
||||
u, err := storage.LFS.URL(pointer.RelativePath(), pointer.Oid, rc.Method, nil)
|
||||
if u != nil && err == nil {
|
||||
// Presigned url does not need the Authorization header
|
||||
// https://github.com/go-gitea/gitea/issues/21525
|
||||
delete(header, "Authorization")
|
||||
link = &lfs_module.Link{Href: u.String(), Header: header}
|
||||
link = lfs_module.NewLink(u.String()) // Presigned url does not need the Authorization header
|
||||
}
|
||||
}
|
||||
if link == nil {
|
||||
link = &lfs_module.Link{Href: rc.DownloadLink(pointer), Header: header}
|
||||
link = lfs_module.NewLink(rc.DownloadLink(pointer)).WithHeader("Authorization", rc.Authorization)
|
||||
}
|
||||
rep.Actions["download"] = link
|
||||
}
|
||||
if upload {
|
||||
rep.Actions["upload"] = &lfs_module.Link{Href: rc.UploadLink(pointer), Header: header}
|
||||
// Set Transfer-Encoding header to enable chunked uploads. Required by git-lfs client to do chunked transfer.
|
||||
// See: https://github.com/git-lfs/git-lfs/blob/main/tq/basic_upload.go#L58-59
|
||||
rep.Actions["upload"] = lfs_module.NewLink(rc.UploadLink(pointer)).
|
||||
WithHeader("Authorization", rc.Authorization).
|
||||
WithHeader("Transfer-Encoding", "chunked")
|
||||
|
||||
verifyHeader := make(map[string]string)
|
||||
maps.Copy(verifyHeader, header)
|
||||
|
||||
// This is only needed to workaround https://github.com/git-lfs/git-lfs/issues/3662
|
||||
verifyHeader["Accept"] = lfs_module.AcceptHeader
|
||||
|
||||
rep.Actions["verify"] = &lfs_module.Link{Href: rc.VerifyLink(pointer), Header: verifyHeader}
|
||||
// "Accept" header is the workaround for git-lfs < 2.8.0 (before 2019).
|
||||
// This workaround could be removed in the future: https://github.com/git-lfs/git-lfs/issues/3662
|
||||
rep.Actions["verify"] = lfs_module.NewLink(rc.VerifyLink(pointer)).
|
||||
WithHeader("Authorization", rc.Authorization).
|
||||
WithHeader("Accept", lfs_module.AcceptHeader)
|
||||
}
|
||||
}
|
||||
return rep
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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())
|
||||
|
||||
|
||||
@@ -6,6 +6,8 @@ package pull
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"code.gitea.io/gitea/models/db"
|
||||
git_model "code.gitea.io/gitea/models/git"
|
||||
@@ -14,8 +16,6 @@ import (
|
||||
"code.gitea.io/gitea/modules/gitrepo"
|
||||
"code.gitea.io/gitea/modules/glob"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
// MergeRequiredContextsCommitStatus returns a commit status state for given required contexts
|
||||
@@ -69,7 +69,7 @@ func MergeRequiredContextsCommitStatus(commitStatuses []*git_model.CommitStatus,
|
||||
func IsPullCommitStatusPass(ctx context.Context, pr *issues_model.PullRequest) (bool, error) {
|
||||
pb, err := git_model.GetFirstMatchProtectedBranchRule(ctx, pr.BaseRepoID, pr.BaseBranch)
|
||||
if err != nil {
|
||||
return false, errors.Wrap(err, "GetLatestCommitStatus")
|
||||
return false, fmt.Errorf("GetLatestCommitStatus: %w", err)
|
||||
}
|
||||
if pb == nil || !pb.EnableStatusCheck {
|
||||
return true, nil
|
||||
@@ -86,19 +86,19 @@ func IsPullCommitStatusPass(ctx context.Context, pr *issues_model.PullRequest) (
|
||||
func GetPullRequestCommitStatusState(ctx context.Context, pr *issues_model.PullRequest) (commitstatus.CommitStatusState, error) {
|
||||
// Ensure HeadRepo is loaded
|
||||
if err := pr.LoadHeadRepo(ctx); err != nil {
|
||||
return "", errors.Wrap(err, "LoadHeadRepo")
|
||||
return "", fmt.Errorf("LoadHeadRepo: %w", err)
|
||||
}
|
||||
|
||||
// check if all required status checks are successful
|
||||
headGitRepo, closer, err := gitrepo.RepositoryFromContextOrOpen(ctx, pr.HeadRepo)
|
||||
if err != nil {
|
||||
return "", errors.Wrap(err, "OpenRepository")
|
||||
return "", fmt.Errorf("OpenRepository: %w", err)
|
||||
}
|
||||
defer closer.Close()
|
||||
|
||||
if pr.Flow == issues_model.PullRequestFlowGithub {
|
||||
if exist, err := git_model.IsBranchExist(ctx, pr.HeadRepo.ID, pr.HeadBranch); err != nil {
|
||||
return "", errors.Wrap(err, "IsBranchExist")
|
||||
return "", fmt.Errorf("IsBranchExist: %w", err)
|
||||
} else if !exist {
|
||||
return "", errors.New("Head branch does not exist, can not merge")
|
||||
}
|
||||
@@ -118,17 +118,17 @@ func GetPullRequestCommitStatusState(ctx context.Context, pr *issues_model.PullR
|
||||
}
|
||||
|
||||
if err := pr.LoadBaseRepo(ctx); err != nil {
|
||||
return "", errors.Wrap(err, "LoadBaseRepo")
|
||||
return "", fmt.Errorf("LoadBaseRepo: %w", err)
|
||||
}
|
||||
|
||||
commitStatuses, err := git_model.GetLatestCommitStatus(ctx, pr.BaseRepo.ID, sha, db.ListOptionsAll)
|
||||
if err != nil {
|
||||
return "", errors.Wrap(err, "GetLatestCommitStatus")
|
||||
return "", fmt.Errorf("GetLatestCommitStatus: %w", err)
|
||||
}
|
||||
|
||||
pb, err := git_model.GetFirstMatchProtectedBranchRule(ctx, pr.BaseRepoID, pr.BaseBranch)
|
||||
if err != nil {
|
||||
return "", errors.Wrap(err, "LoadProtectedBranch")
|
||||
return "", fmt.Errorf("LoadProtectedBranch: %w", err)
|
||||
}
|
||||
var requiredContexts []string
|
||||
if pb != nil {
|
||||
|
||||
@@ -1077,7 +1077,7 @@ func GetPullCommits(ctx context.Context, baseGitRepo *git.Repository, doer *user
|
||||
if pull.HasMerged {
|
||||
baseBranch = pull.MergeBase
|
||||
}
|
||||
compareInfo, err := git_service.GetCompareInfo(ctx, pull.BaseRepo, pull.BaseRepo, baseGitRepo, git.RefNameFromBranch(baseBranch), git.RefName(pull.GetGitHeadRefName()), true, false)
|
||||
compareInfo, err := git_service.GetCompareInfo(ctx, pull.BaseRepo, pull.BaseRepo, baseGitRepo, git.RefNameFromBranch(baseBranch), git.RefName(pull.GetGitHeadRefName()), false, false)
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
|
||||
@@ -8,7 +8,6 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
@@ -16,6 +15,7 @@ import (
|
||||
"code.gitea.io/gitea/models/db"
|
||||
repo_model "code.gitea.io/gitea/models/repo"
|
||||
"code.gitea.io/gitea/modules/git"
|
||||
"code.gitea.io/gitea/modules/git/gitcmd"
|
||||
"code.gitea.io/gitea/modules/gitrepo"
|
||||
"code.gitea.io/gitea/modules/graceful"
|
||||
"code.gitea.io/gitea/modules/httplib"
|
||||
@@ -24,6 +24,7 @@ import (
|
||||
"code.gitea.io/gitea/modules/queue"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/modules/storage"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
gitea_context "code.gitea.io/gitea/services/context"
|
||||
)
|
||||
|
||||
@@ -36,58 +37,31 @@ type ArchiveRequest struct {
|
||||
Repo *repo_model.Repository
|
||||
Type repo_model.ArchiveType
|
||||
CommitID string
|
||||
Paths []string
|
||||
|
||||
archiveRefShortName string // the ref short name to download the archive, for example: "master", "v1.0.0", "commit id"
|
||||
}
|
||||
|
||||
// ErrUnknownArchiveFormat request archive format is not supported
|
||||
type ErrUnknownArchiveFormat struct {
|
||||
RequestNameType string
|
||||
}
|
||||
|
||||
// Error implements error
|
||||
func (err ErrUnknownArchiveFormat) Error() string {
|
||||
return "unknown format: " + err.RequestNameType
|
||||
}
|
||||
|
||||
// Is implements error
|
||||
func (ErrUnknownArchiveFormat) Is(err error) bool {
|
||||
_, ok := err.(ErrUnknownArchiveFormat)
|
||||
return ok
|
||||
}
|
||||
|
||||
// RepoRefNotFoundError is returned when a requested reference (commit, tag) was not found.
|
||||
type RepoRefNotFoundError struct {
|
||||
RefShortName string
|
||||
}
|
||||
|
||||
// Error implements error.
|
||||
func (e RepoRefNotFoundError) Error() string {
|
||||
return "unrecognized repository reference: " + e.RefShortName
|
||||
}
|
||||
|
||||
func (e RepoRefNotFoundError) Is(err error) bool {
|
||||
_, ok := err.(RepoRefNotFoundError)
|
||||
return ok
|
||||
}
|
||||
|
||||
// NewRequest creates an archival request, based on the URI. The
|
||||
// resulting ArchiveRequest is suitable for being passed to Await()
|
||||
// if it's determined that the request still needs to be satisfied.
|
||||
func NewRequest(repo *repo_model.Repository, gitRepo *git.Repository, archiveRefExt string) (*ArchiveRequest, error) {
|
||||
func NewRequest(repo *repo_model.Repository, gitRepo *git.Repository, archiveRefExt string, paths []string) (*ArchiveRequest, error) {
|
||||
// here the archiveRefShortName is not a clear ref, it could be a tag, branch or commit id
|
||||
archiveRefShortName, archiveType := repo_model.SplitArchiveNameType(archiveRefExt)
|
||||
if archiveType == repo_model.ArchiveUnknown {
|
||||
return nil, ErrUnknownArchiveFormat{archiveRefExt}
|
||||
return nil, util.NewInvalidArgumentErrorf("unknown format: %s", archiveRefExt)
|
||||
}
|
||||
if archiveType == repo_model.ArchiveBundle && len(paths) != 0 {
|
||||
return nil, util.NewInvalidArgumentErrorf("cannot specify paths when requesting a bundle")
|
||||
}
|
||||
|
||||
// Get corresponding commit.
|
||||
commitID, err := gitRepo.ConvertToGitID(archiveRefShortName)
|
||||
if err != nil {
|
||||
return nil, RepoRefNotFoundError{RefShortName: archiveRefShortName}
|
||||
return nil, util.NewNotExistErrorf("unrecognized repository reference: %s", archiveRefShortName)
|
||||
}
|
||||
|
||||
r := &ArchiveRequest{Repo: repo, archiveRefShortName: archiveRefShortName, Type: archiveType}
|
||||
r := &ArchiveRequest{Repo: repo, archiveRefShortName: archiveRefShortName, Type: archiveType, Paths: paths}
|
||||
r.CommitID = commitID.String()
|
||||
return r, nil
|
||||
}
|
||||
@@ -159,6 +133,7 @@ func (aReq *ArchiveRequest) Stream(ctx context.Context, w io.Writer) error {
|
||||
w,
|
||||
setting.Repository.PrefixArchiveFiles,
|
||||
aReq.CommitID,
|
||||
aReq.Paths,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -339,7 +314,7 @@ func DeleteRepositoryArchives(ctx context.Context) error {
|
||||
return storage.Clean(storage.RepoArchives)
|
||||
}
|
||||
|
||||
func ServeRepoArchive(ctx *gitea_context.Base, archiveReq *ArchiveRequest) {
|
||||
func ServeRepoArchive(ctx *gitea_context.Base, archiveReq *ArchiveRequest) error {
|
||||
// Add nix format link header so tarballs lock correctly:
|
||||
// https://github.com/nixos/nix/blob/56763ff918eb308db23080e560ed2ea3e00c80a7/doc/manual/src/protocols/tarball-fetcher.md
|
||||
ctx.Resp.Header().Add("Link", fmt.Sprintf(`<%s/archive/%s.%s?rev=%s>; rel="immutable"`,
|
||||
@@ -350,20 +325,22 @@ func ServeRepoArchive(ctx *gitea_context.Base, archiveReq *ArchiveRequest) {
|
||||
))
|
||||
downloadName := archiveReq.Repo.Name + "-" + archiveReq.GetArchiveName()
|
||||
|
||||
if setting.Repository.StreamArchives {
|
||||
if setting.Repository.StreamArchives || len(archiveReq.Paths) > 0 {
|
||||
// the header must be set before starting streaming even an error would occur,
|
||||
// because errors may happen in git command and such cases aren't in our control.
|
||||
httplib.ServeSetHeaders(ctx.Resp, &httplib.ServeHeaderOptions{Filename: downloadName})
|
||||
if err := archiveReq.Stream(ctx, ctx.Resp); err != nil && !ctx.Written() {
|
||||
log.Error("Archive %v streaming failed: %v", archiveReq, err)
|
||||
ctx.HTTPError(http.StatusInternalServerError)
|
||||
if gitcmd.StderrHasPrefix(err, "fatal: pathspec") {
|
||||
return util.NewInvalidArgumentErrorf("path doesn't exist or is invalid")
|
||||
}
|
||||
return fmt.Errorf("archive repo %s: failed to stream: %w", archiveReq.Repo.FullName(), err)
|
||||
}
|
||||
return
|
||||
return nil
|
||||
}
|
||||
|
||||
archiver, err := archiveReq.Await(ctx)
|
||||
if err != nil {
|
||||
log.Error("Archive %v await failed: %v", archiveReq, err)
|
||||
ctx.HTTPError(http.StatusInternalServerError)
|
||||
return
|
||||
return fmt.Errorf("archive repo %s: failed to await: %w", archiveReq.Repo.FullName(), err)
|
||||
}
|
||||
|
||||
rPath := archiver.RelativePath()
|
||||
@@ -372,15 +349,13 @@ func ServeRepoArchive(ctx *gitea_context.Base, archiveReq *ArchiveRequest) {
|
||||
u, err := storage.RepoArchives.URL(rPath, downloadName, ctx.Req.Method, nil)
|
||||
if u != nil && err == nil {
|
||||
ctx.Redirect(u.String())
|
||||
return
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
fr, err := storage.RepoArchives.Open(rPath)
|
||||
if err != nil {
|
||||
log.Error("Archive %v open file failed: %v", archiveReq, err)
|
||||
ctx.HTTPError(http.StatusInternalServerError)
|
||||
return
|
||||
return fmt.Errorf("archive repo %s: failed to open archive file: %w", archiveReq.Repo.FullName(), err)
|
||||
}
|
||||
defer fr.Close()
|
||||
|
||||
@@ -388,4 +363,5 @@ func ServeRepoArchive(ctx *gitea_context.Base, archiveReq *ArchiveRequest) {
|
||||
Filename: downloadName,
|
||||
LastModified: archiver.CreatedUnix.AsLocalTime(),
|
||||
})
|
||||
return nil
|
||||
}
|
||||
@@ -8,11 +8,13 @@ import (
|
||||
"time"
|
||||
|
||||
"code.gitea.io/gitea/models/unittest"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
"code.gitea.io/gitea/services/contexttest"
|
||||
|
||||
_ "code.gitea.io/gitea/models/actions"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
@@ -29,47 +31,47 @@ func TestArchive_Basic(t *testing.T) {
|
||||
contexttest.LoadGitRepo(t, ctx)
|
||||
defer ctx.Repo.GitRepo.Close()
|
||||
|
||||
bogusReq, err := NewRequest(ctx.Repo.Repository, ctx.Repo.GitRepo, firstCommit+".zip")
|
||||
bogusReq, err := NewRequest(ctx.Repo.Repository, ctx.Repo.GitRepo, firstCommit+".zip", nil)
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, bogusReq)
|
||||
assert.Equal(t, firstCommit+".zip", bogusReq.GetArchiveName())
|
||||
|
||||
// Check a series of bogus requests.
|
||||
// Step 1, valid commit with a bad extension.
|
||||
bogusReq, err = NewRequest(ctx.Repo.Repository, ctx.Repo.GitRepo, firstCommit+".unknown")
|
||||
bogusReq, err = NewRequest(ctx.Repo.Repository, ctx.Repo.GitRepo, firstCommit+".unknown", nil)
|
||||
assert.Error(t, err)
|
||||
assert.Nil(t, bogusReq)
|
||||
|
||||
// Step 2, missing commit.
|
||||
bogusReq, err = NewRequest(ctx.Repo.Repository, ctx.Repo.GitRepo, "dbffff.zip")
|
||||
bogusReq, err = NewRequest(ctx.Repo.Repository, ctx.Repo.GitRepo, "dbffff.zip", nil)
|
||||
assert.Error(t, err)
|
||||
assert.Nil(t, bogusReq)
|
||||
|
||||
// Step 3, doesn't look like branch/tag/commit.
|
||||
bogusReq, err = NewRequest(ctx.Repo.Repository, ctx.Repo.GitRepo, "db.zip")
|
||||
bogusReq, err = NewRequest(ctx.Repo.Repository, ctx.Repo.GitRepo, "db.zip", nil)
|
||||
assert.Error(t, err)
|
||||
assert.Nil(t, bogusReq)
|
||||
|
||||
bogusReq, err = NewRequest(ctx.Repo.Repository, ctx.Repo.GitRepo, "master.zip")
|
||||
bogusReq, err = NewRequest(ctx.Repo.Repository, ctx.Repo.GitRepo, "master.zip", nil)
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, bogusReq)
|
||||
assert.Equal(t, "master.zip", bogusReq.GetArchiveName())
|
||||
|
||||
bogusReq, err = NewRequest(ctx.Repo.Repository, ctx.Repo.GitRepo, "test/archive.zip")
|
||||
bogusReq, err = NewRequest(ctx.Repo.Repository, ctx.Repo.GitRepo, "test/archive.zip", nil)
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, bogusReq)
|
||||
assert.Equal(t, "test-archive.zip", bogusReq.GetArchiveName())
|
||||
|
||||
// Now two valid requests, firstCommit with valid extensions.
|
||||
zipReq, err := NewRequest(ctx.Repo.Repository, ctx.Repo.GitRepo, firstCommit+".zip")
|
||||
zipReq, err := NewRequest(ctx.Repo.Repository, ctx.Repo.GitRepo, firstCommit+".zip", nil)
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, zipReq)
|
||||
|
||||
tgzReq, err := NewRequest(ctx.Repo.Repository, ctx.Repo.GitRepo, firstCommit+".tar.gz")
|
||||
tgzReq, err := NewRequest(ctx.Repo.Repository, ctx.Repo.GitRepo, firstCommit+".tar.gz", nil)
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, tgzReq)
|
||||
|
||||
secondReq, err := NewRequest(ctx.Repo.Repository, ctx.Repo.GitRepo, secondCommit+".bundle")
|
||||
secondReq, err := NewRequest(ctx.Repo.Repository, ctx.Repo.GitRepo, secondCommit+".bundle", nil)
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, secondReq)
|
||||
|
||||
@@ -89,7 +91,7 @@ func TestArchive_Basic(t *testing.T) {
|
||||
// Sleep two seconds to make sure the queue doesn't change.
|
||||
time.Sleep(2 * time.Second)
|
||||
|
||||
zipReq2, err := NewRequest(ctx.Repo.Repository, ctx.Repo.GitRepo, firstCommit+".zip")
|
||||
zipReq2, err := NewRequest(ctx.Repo.Repository, ctx.Repo.GitRepo, firstCommit+".zip", nil)
|
||||
assert.NoError(t, err)
|
||||
// This zipReq should match what's sitting in the queue, as we haven't
|
||||
// let it release yet. From the consumer's point of view, this looks like
|
||||
@@ -104,12 +106,12 @@ func TestArchive_Basic(t *testing.T) {
|
||||
// Now we'll submit a request and TimedWaitForCompletion twice, before and
|
||||
// after we release it. We should trigger both the timeout and non-timeout
|
||||
// cases.
|
||||
timedReq, err := NewRequest(ctx.Repo.Repository, ctx.Repo.GitRepo, secondCommit+".tar.gz")
|
||||
timedReq, err := NewRequest(ctx.Repo.Repository, ctx.Repo.GitRepo, secondCommit+".tar.gz", nil)
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, timedReq)
|
||||
doArchive(t.Context(), timedReq)
|
||||
|
||||
zipReq2, err = NewRequest(ctx.Repo.Repository, ctx.Repo.GitRepo, firstCommit+".zip")
|
||||
zipReq2, err = NewRequest(ctx.Repo.Repository, ctx.Repo.GitRepo, firstCommit+".zip", nil)
|
||||
assert.NoError(t, err)
|
||||
// Now, we're guaranteed to have released the original zipReq from the queue.
|
||||
// Ensure that we don't get handed back the released entry somehow, but they
|
||||
@@ -124,9 +126,13 @@ func TestArchive_Basic(t *testing.T) {
|
||||
// Ideally, the extension would match what we originally requested.
|
||||
assert.NotEqual(t, zipReq.GetArchiveName(), tgzReq.GetArchiveName())
|
||||
assert.NotEqual(t, zipReq.GetArchiveName(), secondReq.GetArchiveName())
|
||||
}
|
||||
|
||||
func TestErrUnknownArchiveFormat(t *testing.T) {
|
||||
err := ErrUnknownArchiveFormat{RequestNameType: "xxx"}
|
||||
assert.ErrorIs(t, err, ErrUnknownArchiveFormat{})
|
||||
t.Run("BadPath", func(t *testing.T) {
|
||||
badRequest, err := NewRequest(ctx.Repo.Repository, ctx.Repo.GitRepo, firstCommit+".tar.gz", []string{"not-a-path"})
|
||||
require.NoError(t, err)
|
||||
err = ServeRepoArchive(ctx.Base, badRequest)
|
||||
require.Error(t, err)
|
||||
assert.ErrorIs(t, err, util.ErrInvalidArgument)
|
||||
assert.ErrorContains(t, err, "path doesn't exist or is invalid")
|
||||
})
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -194,6 +194,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
|
||||
@@ -217,28 +221,25 @@ 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) {
|
||||
// GetAttachmentLinkedType returns the linked type of attachment if any
|
||||
func GetAttachmentLinkedType(ctx context.Context, a *repo_model.Attachment) (unit.Type, error) {
|
||||
if a.IssueID != 0 {
|
||||
iss, err := issues_model.GetIssueByID(ctx, a.IssueID)
|
||||
if err != nil {
|
||||
return nil, unit.TypeIssues, err
|
||||
return unit.TypeIssues, 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 {
|
||||
rel, err := repo_model.GetReleaseByID(ctx, a.ReleaseID)
|
||||
if err != nil {
|
||||
return nil, unit.TypeReleases, err
|
||||
}
|
||||
repo, err := repo_model.GetRepositoryByID(ctx, rel.RepoID)
|
||||
return repo, unit.TypeReleases, err
|
||||
return unitType, nil
|
||||
}
|
||||
return nil, -1, nil
|
||||
|
||||
if a.ReleaseID != 0 {
|
||||
_, err := repo_model.GetReleaseByID(ctx, a.ReleaseID)
|
||||
return unit.TypeReleases, err
|
||||
}
|
||||
return unit.TypeInvalid, nil
|
||||
}
|
||||
|
||||
// CheckDaemonExportOK creates/removes git-daemon-export-ok for git-daemon...
|
||||
|
||||
@@ -13,30 +13,27 @@ import (
|
||||
"code.gitea.io/gitea/models/unittest"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestLinkedRepository(t *testing.T) {
|
||||
func TestAttachLinkedType(t *testing.T) {
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
testCases := []struct {
|
||||
name string
|
||||
attachID int64
|
||||
expectedRepo *repo_model.Repository
|
||||
expectedUnitType unit.Type
|
||||
}{
|
||||
{"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},
|
||||
{"LinkedComment", 3, unit.TypePullRequests},
|
||||
{"LinkedRelease", 9, unit.TypeReleases},
|
||||
{"Notlinked", 10, unit.TypeInvalid},
|
||||
}
|
||||
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, err := GetAttachmentLinkedType(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)
|
||||
})
|
||||
}
|
||||
@@ -70,3 +67,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)
|
||||
}
|
||||
@@ -239,6 +239,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
|
||||
@@ -269,6 +274,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)
|
||||
|
||||
@@ -63,6 +63,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())
|
||||
|
||||
Reference in new issue
Block a user