Skip to content

Commit 30b1394

Browse files
job79wxiaoguang
andauthored
Give organisation members access to organisation feeds (#33508)
Currently the organisation feed only includes items for public repositories (for non-administrators). This pull requests adds notifications from private repositories to the organisation-feed (for accounts that have access to the organisation). Feed-items only get shown for repositories where the users team(s) should have access to, this filtering seems to get done by some existing code. Needs some tests, but am unsure where/how to add them. Before: ![image](https://github.com/user-attachments/assets/8b63f430-227a-4b19-ad1a-f6f5175de301) After: ![image](https://github.com/user-attachments/assets/b439ce0e-4946-421c-a399-421806c7a6d8) --------- Co-authored-by: wxiaoguang <[email protected]>
1 parent 3e2e7bf commit 30b1394

File tree

6 files changed

+110
-50
lines changed

6 files changed

+110
-50
lines changed

models/unittest/fixtures_loader.go

+53-29
Original file line numberDiff line numberDiff line change
@@ -12,13 +12,17 @@ import (
1212
"slices"
1313
"strings"
1414

15+
"code.gitea.io/gitea/models/db"
16+
1517
"gopkg.in/yaml.v3"
1618
"xorm.io/xorm"
1719
"xorm.io/xorm/schemas"
1820
)
1921

20-
type fixtureItem struct {
21-
tableName string
22+
type FixtureItem struct {
23+
fileFullPath string
24+
tableName string
25+
2226
tableNameQuoted string
2327
sqlInserts []string
2428
sqlInsertArgs [][]any
@@ -27,10 +31,11 @@ type fixtureItem struct {
2731
}
2832

2933
type fixturesLoaderInternal struct {
34+
xormEngine *xorm.Engine
35+
xormTableNames map[string]bool
3036
db *sql.DB
3137
dbType schemas.DBType
32-
files []string
33-
fixtures map[string]*fixtureItem
38+
fixtures map[string]*FixtureItem
3439
quoteObject func(string) string
3540
paramPlaceholder func(idx int) string
3641
}
@@ -59,29 +64,27 @@ func (f *fixturesLoaderInternal) preprocessFixtureRow(row []map[string]any) (err
5964
return nil
6065
}
6166

62-
func (f *fixturesLoaderInternal) prepareFixtureItem(file string) (_ *fixtureItem, err error) {
63-
fixture := &fixtureItem{}
64-
fixture.tableName, _, _ = strings.Cut(filepath.Base(file), ".")
67+
func (f *fixturesLoaderInternal) prepareFixtureItem(fixture *FixtureItem) (err error) {
6568
fixture.tableNameQuoted = f.quoteObject(fixture.tableName)
6669

6770
if f.dbType == schemas.MSSQL {
6871
fixture.mssqlHasIdentityColumn, err = f.mssqlTableHasIdentityColumn(f.db, fixture.tableName)
6972
if err != nil {
70-
return nil, err
73+
return err
7174
}
7275
}
7376

74-
data, err := os.ReadFile(file)
77+
data, err := os.ReadFile(fixture.fileFullPath)
7578
if err != nil {
76-
return nil, fmt.Errorf("failed to read file %q: %w", file, err)
79+
return fmt.Errorf("failed to read file %q: %w", fixture.fileFullPath, err)
7780
}
7881

7982
var rows []map[string]any
8083
if err = yaml.Unmarshal(data, &rows); err != nil {
81-
return nil, fmt.Errorf("failed to unmarshal yaml data from %q: %w", file, err)
84+
return fmt.Errorf("failed to unmarshal yaml data from %q: %w", fixture.fileFullPath, err)
8285
}
8386
if err = f.preprocessFixtureRow(rows); err != nil {
84-
return nil, fmt.Errorf("failed to preprocess fixture rows from %q: %w", file, err)
87+
return fmt.Errorf("failed to preprocess fixture rows from %q: %w", fixture.fileFullPath, err)
8588
}
8689

8790
var sqlBuf []byte
@@ -107,16 +110,14 @@ func (f *fixturesLoaderInternal) prepareFixtureItem(file string) (_ *fixtureItem
107110
sqlBuf = sqlBuf[:0]
108111
sqlArguments = sqlArguments[:0]
109112
}
110-
return fixture, nil
113+
return nil
111114
}
112115

113-
func (f *fixturesLoaderInternal) loadFixtures(tx *sql.Tx, file string) (err error) {
114-
fixture := f.fixtures[file]
115-
if fixture == nil {
116-
if fixture, err = f.prepareFixtureItem(file); err != nil {
116+
func (f *fixturesLoaderInternal) loadFixtures(tx *sql.Tx, fixture *FixtureItem) (err error) {
117+
if fixture.tableNameQuoted == "" {
118+
if err = f.prepareFixtureItem(fixture); err != nil {
117119
return err
118120
}
119-
f.fixtures[file] = fixture
120121
}
121122

122123
_, err = tx.Exec(fmt.Sprintf("DELETE FROM %s", fixture.tableNameQuoted)) // sqlite3 doesn't support truncate
@@ -147,15 +148,26 @@ func (f *fixturesLoaderInternal) Load() error {
147148
}
148149
defer func() { _ = tx.Rollback() }()
149150

150-
for _, file := range f.files {
151-
if err := f.loadFixtures(tx, file); err != nil {
152-
return fmt.Errorf("failed to load fixtures from %s: %w", file, err)
151+
for _, fixture := range f.fixtures {
152+
if !f.xormTableNames[fixture.tableName] {
153+
continue
154+
}
155+
if err := f.loadFixtures(tx, fixture); err != nil {
156+
return fmt.Errorf("failed to load fixtures from %s: %w", fixture.fileFullPath, err)
153157
}
154158
}
155-
return tx.Commit()
159+
if err = tx.Commit(); err != nil {
160+
return err
161+
}
162+
for xormTableName := range f.xormTableNames {
163+
if f.fixtures[xormTableName] == nil {
164+
_, _ = f.xormEngine.Exec("DELETE FROM `" + xormTableName + "`")
165+
}
166+
}
167+
return nil
156168
}
157169

158-
func FixturesFileFullPaths(dir string, files []string) ([]string, error) {
170+
func FixturesFileFullPaths(dir string, files []string) (map[string]*FixtureItem, error) {
159171
if files != nil && len(files) == 0 {
160172
return nil, nil // load nothing
161173
}
@@ -169,20 +181,25 @@ func FixturesFileFullPaths(dir string, files []string) ([]string, error) {
169181
files = append(files, e.Name())
170182
}
171183
}
172-
for i, file := range files {
173-
if !filepath.IsAbs(file) {
174-
files[i] = filepath.Join(dir, file)
184+
fixtureItems := map[string]*FixtureItem{}
185+
for _, file := range files {
186+
fileFillPath := file
187+
if !filepath.IsAbs(fileFillPath) {
188+
fileFillPath = filepath.Join(dir, file)
175189
}
190+
tableName, _, _ := strings.Cut(filepath.Base(file), ".")
191+
fixtureItems[tableName] = &FixtureItem{fileFullPath: fileFillPath, tableName: tableName}
176192
}
177-
return files, nil
193+
return fixtureItems, nil
178194
}
179195

180196
func NewFixturesLoader(x *xorm.Engine, opts FixturesOptions) (FixturesLoader, error) {
181-
files, err := FixturesFileFullPaths(opts.Dir, opts.Files)
197+
fixtureItems, err := FixturesFileFullPaths(opts.Dir, opts.Files)
182198
if err != nil {
183199
return nil, fmt.Errorf("failed to get fixtures files: %w", err)
184200
}
185-
f := &fixturesLoaderInternal{db: x.DB().DB, dbType: x.Dialect().URI().DBType, files: files, fixtures: map[string]*fixtureItem{}}
201+
202+
f := &fixturesLoaderInternal{xormEngine: x, db: x.DB().DB, dbType: x.Dialect().URI().DBType, fixtures: fixtureItems}
186203
switch f.dbType {
187204
case schemas.SQLITE:
188205
f.quoteObject = func(s string) string { return fmt.Sprintf(`"%s"`, s) }
@@ -197,5 +214,12 @@ func NewFixturesLoader(x *xorm.Engine, opts FixturesOptions) (FixturesLoader, er
197214
f.quoteObject = func(s string) string { return fmt.Sprintf("[%s]", s) }
198215
f.paramPlaceholder = func(idx int) string { return "?" }
199216
}
217+
218+
xormBeans, _ := db.NamesToBean()
219+
f.xormTableNames = map[string]bool{}
220+
for _, bean := range xormBeans {
221+
f.xormTableNames[db.TableName(bean)] = true
222+
}
223+
200224
return f, nil
201225
}

routers/web/feed/profile.go

+13-1
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import (
77
"time"
88

99
activities_model "code.gitea.io/gitea/models/activities"
10+
"code.gitea.io/gitea/models/organization"
1011
"code.gitea.io/gitea/models/renderhelper"
1112
"code.gitea.io/gitea/modules/markup/markdown"
1213
"code.gitea.io/gitea/services/context"
@@ -28,12 +29,23 @@ func ShowUserFeedAtom(ctx *context.Context) {
2829
// showUserFeed show user activity as RSS / Atom feed
2930
func showUserFeed(ctx *context.Context, formatType string) {
3031
includePrivate := ctx.IsSigned && (ctx.Doer.IsAdmin || ctx.Doer.ID == ctx.ContextUser.ID)
32+
isOrganisation := ctx.ContextUser.IsOrganization()
33+
if ctx.IsSigned && isOrganisation && !includePrivate {
34+
// When feed is requested by a member of the organization,
35+
// include the private repo's the member has access to.
36+
isOrgMember, err := organization.IsOrganizationMember(ctx, ctx.ContextUser.ID, ctx.Doer.ID)
37+
if err != nil {
38+
ctx.ServerError("IsOrganizationMember", err)
39+
return
40+
}
41+
includePrivate = isOrgMember
42+
}
3143

3244
actions, _, err := feed_service.GetFeeds(ctx, activities_model.GetFeedsOptions{
3345
RequestedUser: ctx.ContextUser,
3446
Actor: ctx.Doer,
3547
IncludePrivate: includePrivate,
36-
OnlyPerformedBy: !ctx.ContextUser.IsOrganization(),
48+
OnlyPerformedBy: !isOrganisation,
3749
IncludeDeleted: false,
3850
Date: ctx.FormString("date"),
3951
})

routers/web/feed/profile_test.go

+38
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
// Copyright 2025 The Gitea Authors. All rights reserved.
2+
// SPDX-License-Identifier: MIT
3+
package feed_test
4+
5+
import (
6+
"testing"
7+
8+
"code.gitea.io/gitea/models/unittest"
9+
user_model "code.gitea.io/gitea/models/user"
10+
"code.gitea.io/gitea/routers/web/feed"
11+
"code.gitea.io/gitea/services/contexttest"
12+
13+
"github.com/stretchr/testify/assert"
14+
)
15+
16+
func TestMain(m *testing.M) {
17+
unittest.MainTest(m)
18+
}
19+
20+
func TestCheckGetOrgFeedsAsOrgMember(t *testing.T) {
21+
unittest.PrepareTestEnv(t)
22+
t.Run("OrgMember", func(t *testing.T) {
23+
ctx, resp := contexttest.MockContext(t, "org3.atom")
24+
ctx.ContextUser = unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 3})
25+
contexttest.LoadUser(t, ctx, 2)
26+
ctx.IsSigned = true
27+
feed.ShowUserFeedAtom(ctx)
28+
assert.Contains(t, resp.Body.String(), "<entry>") // Should contain 1 private entry
29+
})
30+
t.Run("NonOrgMember", func(t *testing.T) {
31+
ctx, resp := contexttest.MockContext(t, "org3.atom")
32+
ctx.ContextUser = unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 3})
33+
contexttest.LoadUser(t, ctx, 5)
34+
ctx.IsSigned = true
35+
feed.ShowUserFeedAtom(ctx)
36+
assert.NotContains(t, resp.Body.String(), "<entry>") // Should not contain any entries
37+
})
38+
}

tests/integration/actions_job_test.go

+4-13
Original file line numberDiff line numberDiff line change
@@ -166,9 +166,6 @@ jobs:
166166
}
167167
})
168168
}
169-
170-
httpContext := NewAPITestContext(t, user2.Name, apiRepo.Name, auth_model.AccessTokenScopeWriteRepository)
171-
doAPIDeleteRepository(httpContext)(t)
172169
})
173170
}
174171

@@ -348,9 +345,6 @@ jobs:
348345
}
349346
})
350347
}
351-
352-
httpContext := NewAPITestContext(t, user2.Name, apiRepo.Name, auth_model.AccessTokenScopeWriteRepository)
353-
doAPIDeleteRepository(httpContext)(t)
354348
})
355349
}
356350

@@ -434,8 +428,6 @@ jobs:
434428
assert.Equal(t, setting.Actions.DefaultActionsURL.URL(), gtCtx["gitea_default_actions_url"].GetStringValue())
435429
token := gtCtx["token"].GetStringValue()
436430
assert.Equal(t, actionTask.TokenLastEight, token[len(token)-8:])
437-
438-
doAPIDeleteRepository(user2APICtx)(t)
439431
})
440432
}
441433

@@ -543,12 +535,14 @@ jobs:
543535
err = actions_service.CleanupEphemeralRunners(t.Context())
544536
assert.NoError(t, err)
545537

546-
runner.client.runnerServiceClient.UpdateTask(t.Context(), connect.NewRequest(&runnerv1.UpdateTaskRequest{
538+
_, err = runner.client.runnerServiceClient.UpdateTask(t.Context(), connect.NewRequest(&runnerv1.UpdateTaskRequest{
547539
State: &runnerv1.TaskState{
548540
Id: actionTask.ID,
549541
Result: runnerv1.Result_RESULT_SUCCESS,
550542
},
551543
}))
544+
assert.NoError(t, err)
545+
552546
resp, err = runner.client.runnerServiceClient.FetchTask(t.Context(), connect.NewRequest(&runnerv1.FetchTaskRequest{
553547
TasksVersion: 0,
554548
}))
@@ -561,7 +555,7 @@ jobs:
561555
assert.Error(t, err)
562556
assert.Nil(t, resp)
563557

564-
// create an runner that picks a job and get force cancelled
558+
// create a runner that picks a job and get force cancelled
565559
runnerToBeRemoved := newMockRunner()
566560
runnerToBeRemoved.registerAsRepoRunner(t, baseRepo.OwnerName, baseRepo.Name, "mock-runner-to-be-removed", []string{"ubuntu-latest"}, true)
567561

@@ -583,9 +577,6 @@ jobs:
583577
assert.NoError(t, err)
584578

585579
unittest.AssertNotExistsBean(t, &actions_model.ActionRunner{ID: runnerToRemove.ID})
586-
587-
// this cleanup is required to allow further tests to pass
588-
doAPIDeleteRepository(user2APICtx)(t)
589580
})
590581
}
591582

tests/integration/actions_log_test.go

+2-5
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ func TestDownloadTaskLogs(t *testing.T) {
3535
{
3636
treePath: ".gitea/workflows/download-task-logs-zstd.yml",
3737
fileContent: `name: download-task-logs-zstd
38-
on:
38+
on:
3939
push:
4040
paths:
4141
- '.gitea/workflows/download-task-logs-zstd.yml'
@@ -67,7 +67,7 @@ jobs:
6767
{
6868
treePath: ".gitea/workflows/download-task-logs-no-zstd.yml",
6969
fileContent: `name: download-task-logs-no-zstd
70-
on:
70+
on:
7171
push:
7272
paths:
7373
- '.gitea/workflows/download-task-logs-no-zstd.yml'
@@ -152,8 +152,5 @@ jobs:
152152
resetFunc()
153153
})
154154
}
155-
156-
httpContext := NewAPITestContext(t, user2.Name, repo.Name, auth_model.AccessTokenScopeWriteRepository)
157-
doAPIDeleteRepository(httpContext)(t)
158155
})
159156
}

tests/integration/api_repo_file_get_test.go

-2
Original file line numberDiff line numberDiff line change
@@ -44,8 +44,6 @@ func TestAPIGetRawFileOrLFS(t *testing.T) {
4444
reqLFS := NewRequest(t, "GET", "/api/v1/repos/user2/repo1/media/"+lfs)
4545
respLFS := MakeRequestNilResponseRecorder(t, reqLFS, http.StatusOK)
4646
assert.Equal(t, testFileSizeSmall, respLFS.Length)
47-
48-
doAPIDeleteRepository(httpContext)
4947
})
5048
})
5149
}

0 commit comments

Comments
 (0)