Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 13 additions & 7 deletions models/repo/release.go
Original file line number Diff line number Diff line change
Expand Up @@ -93,15 +93,21 @@ func init() {
db.RegisterModel(new(Release))
}

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) error {
var err error
if r.Repo == nil {
r.Repo, err = GetRepositoryByID(ctx, r.RepoID)
if err != nil {
return err
}
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 {
Expand Down
10 changes: 10 additions & 0 deletions models/repo/watch.go
Original file line number Diff line number Diff line change
Expand Up @@ -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})
}
19 changes: 19 additions & 0 deletions models/repo/watch_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -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)
}
13 changes: 13 additions & 0 deletions services/mailer/mail_release.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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 {
Expand Down
71 changes: 71 additions & 0 deletions services/mailer/mail_release_test.go
Original file line number Diff line number Diff line change
@@ -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)
}
4 changes: 4 additions & 0 deletions services/repository/repository.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
22 changes: 22 additions & 0 deletions services/repository/repository_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import (
"code.gitea.io/gitea/models/unittest"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

func TestLinkedRepository(t *testing.T) {
Expand Down Expand Up @@ -70,3 +71,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)
}