diff --git a/.deadcode-out b/.deadcode-out index 61c5bcb055..e63e4a3dc3 100644 --- a/.deadcode-out +++ b/.deadcode-out @@ -13,13 +13,6 @@ forgejo.org/models IsErrSHANotFound IsErrMergeDivergingFastForwardOnly -forgejo.org/models/activities - GetActivityByID - NewFederatedUserActivity - CreateUserActivity - GetFollowingFeeds - FederatedUserActivity.loadActor - forgejo.org/models/auth WebAuthnCredentials @@ -61,17 +54,9 @@ forgejo.org/models/user IsErrExternalLoginUserAlreadyExist IsErrExternalLoginUserNotExist NewFederatedUser - NewFederatedUserFollower IsErrUserSettingIsNotExist GetUserAllSettings DeleteUserSetting - GetFederatedUser - GetFederatedUserByUserID - UpdateFederatedUser - GetFollowersForUser - AddFollower - RemoveFollower - IsFollowingAp forgejo.org/modules/activitypub NewContext diff --git a/.forgejo/workflows/renovate.yml b/.forgejo/workflows/renovate.yml index 5aa6c8cd98..98b93b5757 100644 --- a/.forgejo/workflows/renovate.yml +++ b/.forgejo/workflows/renovate.yml @@ -28,7 +28,7 @@ jobs: runs-on: docker container: - image: data.forgejo.org/renovate/renovate:41.1.4 + image: data.forgejo.org/renovate/renovate:40.57.1 steps: - name: Load renovate repo cache diff --git a/.forgejo/workflows/testing.yml b/.forgejo/workflows/testing.yml index 7a93bb66a8..86e74591f1 100644 --- a/.forgejo/workflows/testing.yml +++ b/.forgejo/workflows/testing.yml @@ -115,11 +115,6 @@ jobs: run: | su forgejo -c 'make deps-frontend frontend' - uses: ./.forgejo/workflows-composite/build-backend - - name: Decide to run all tests - id: run-all - if: contains(github.event.pull_request.labels.*.name, 'run-all-playwright-tests') || contains(github.event.pull_request.title, 'playwright') - run: | - echo "all=1" >> "$GITHUB_OUTPUT" - name: Get changed files id: changed-files uses: https://data.forgejo.org/tj-actions/changed-files@v46 @@ -132,7 +127,6 @@ jobs: USE_REPO_TEST_DIR: 1 PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: 1 CHANGED_FILES: ${{steps.changed-files.outputs.all_changed_files}} - RUN_ALL: ${{steps.run-all.all}} - name: Upload test artifacts on failure if: failure() uses: https://data.forgejo.org/forgejo/upload-artifact@v4 diff --git a/Makefile b/Makefile index e770f2a989..852c85ccd6 100644 --- a/Makefile +++ b/Makefile @@ -47,7 +47,7 @@ GO_LICENSES_PACKAGE ?= github.com/google/go-licenses@v1.6.0 # renovate: datasour GOVULNCHECK_PACKAGE ?= golang.org/x/vuln/cmd/govulncheck@v1 # renovate: datasource=go DEADCODE_PACKAGE ?= golang.org/x/tools/cmd/deadcode@v0.34.0 # renovate: datasource=go GOMOCK_PACKAGE ?= go.uber.org/mock/mockgen@v0.5.2 # renovate: datasource=go -RENOVATE_NPM_PACKAGE ?= renovate@41.1.4 # renovate: datasource=docker packageName=data.forgejo.org/renovate/renovate +RENOVATE_NPM_PACKAGE ?= renovate@40.57.1 # renovate: datasource=docker packageName=data.forgejo.org/renovate/renovate # https://github.com/disposable-email-domains/disposable-email-domains/commits/main/ DISPOSABLE_EMAILS_SHA ?= 0c27e671231d27cf66370034d7f6818037416989 # renovate: ... diff --git a/go.mod b/go.mod index bb2be827eb..87755b206a 100644 --- a/go.mod +++ b/go.mod @@ -41,7 +41,7 @@ require ( github.com/gliderlabs/ssh v0.3.8 github.com/go-ap/activitypub v0.0.0-20231114162308-e219254dc5c9 github.com/go-ap/jsonld v0.0.0-20221030091449-f2a191312c73 - github.com/go-chi/chi/v5 v5.2.2 + github.com/go-chi/chi/v5 v5.2.1 github.com/go-chi/cors v1.2.1 github.com/go-co-op/gocron v1.37.0 github.com/go-enry/go-enry/v2 v2.9.2 diff --git a/go.sum b/go.sum index 639880e2ce..54710930e8 100644 --- a/go.sum +++ b/go.sum @@ -213,8 +213,8 @@ github.com/go-ap/jsonld v0.0.0-20221030091449-f2a191312c73/go.mod h1:jyveZeGw5La github.com/go-asn1-ber/asn1-ber v1.5.5 h1:MNHlNMBDgEKD4TcKr36vQN68BA00aDfjIt3/bD50WnA= github.com/go-asn1-ber/asn1-ber v1.5.5/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0= github.com/go-chi/chi/v5 v5.0.1/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8= -github.com/go-chi/chi/v5 v5.2.2 h1:CMwsvRVTbXVytCk1Wd72Zy1LAsAh9GxMmSNWLHCG618= -github.com/go-chi/chi/v5 v5.2.2/go.mod h1:L2yAIGWB3H+phAw1NxKwWM+7eUH/lU8pOMm5hHcoops= +github.com/go-chi/chi/v5 v5.2.1 h1:KOIHODQj58PmL80G2Eak4WdvUzjSJSm0vG72crDCqb8= +github.com/go-chi/chi/v5 v5.2.1/go.mod h1:L2yAIGWB3H+phAw1NxKwWM+7eUH/lU8pOMm5hHcoops= github.com/go-chi/cors v1.2.1 h1:xEC8UT3Rlp2QuWNEr4Fs/c2EAGVKBwy/1vHx3bppil4= github.com/go-chi/cors v1.2.1/go.mod h1:sSbTewc+6wYHBBCW7ytsFSn836hqM7JxpglAy2Vzc58= github.com/go-co-op/gocron v1.37.0 h1:ZYDJGtQ4OMhTLKOKMIch+/CY70Brbb1dGdooLEhh7b0= diff --git a/models/actions/run.go b/models/actions/run.go index 55def805ed..48756b7a08 100644 --- a/models/actions/run.go +++ b/models/actions/run.go @@ -55,7 +55,6 @@ type ActionRun struct { PreviousDuration time.Duration Created timeutil.TimeStamp `xorm:"created"` Updated timeutil.TimeStamp `xorm:"updated"` - NotifyEmail bool } func init() { diff --git a/models/activities/action.go b/models/activities/action.go index 8592f81414..1e40546b97 100644 --- a/models/activities/action.go +++ b/models/activities/action.go @@ -442,12 +442,6 @@ func (a *Action) GetIssueContent(ctx context.Context) string { return a.Issue.Content } -func GetActivityByID(ctx context.Context, id int64) (*Action, error) { - var act Action - _, err := db.GetEngine(ctx).ID(id).Get(&act) - return &act, err -} - // GetFeedsOptions options for retrieving feeds type GetFeedsOptions struct { db.ListOptions @@ -601,14 +595,13 @@ func DeleteOldActions(ctx context.Context, olderThan time.Duration) (err error) } // NotifyWatchers creates batch of actions for every watcher. -func NotifyWatchers(ctx context.Context, actions ...*Action) ([]Action, error) { +func NotifyWatchers(ctx context.Context, actions ...*Action) error { var watchers []*repo_model.Watch var repo *repo_model.Repository var err error var permCode []bool var permIssue []bool var permPR []bool - var out []Action e := db.GetEngine(ctx) @@ -619,14 +612,14 @@ func NotifyWatchers(ctx context.Context, actions ...*Action) ([]Action, error) { // Add feeds for user self and all watchers. watchers, err = repo_model.GetWatchers(ctx, act.RepoID) if err != nil { - return nil, fmt.Errorf("get watchers: %w", err) + return fmt.Errorf("get watchers: %w", err) } // Be aware that optimizing this correctly into the `GetWatchers` SQL // query is for most cases less performant than doing this. blockedDoerUserIDs, err := user_model.ListBlockedByUsersID(ctx, act.ActUserID) if err != nil { - return nil, fmt.Errorf("user_model.ListBlockedByUsersID: %w", err) + return fmt.Errorf("user_model.ListBlockedByUsersID: %w", err) } if len(blockedDoerUserIDs) > 0 { @@ -641,9 +634,8 @@ func NotifyWatchers(ctx context.Context, actions ...*Action) ([]Action, error) { // Add feed for actioner. act.UserID = act.ActUserID if _, err = e.Insert(act); err != nil { - return nil, fmt.Errorf("insert new actioner: %w", err) + return fmt.Errorf("insert new actioner: %w", err) } - out = append(out, *act) if repoChanged { act.loadRepo(ctx) @@ -651,7 +643,7 @@ func NotifyWatchers(ctx context.Context, actions ...*Action) ([]Action, error) { // check repo owner exist. if err := act.Repo.LoadOwner(ctx); err != nil { - return nil, fmt.Errorf("can't get repo owner: %w", err) + return fmt.Errorf("can't get repo owner: %w", err) } } else if act.Repo == nil { act.Repo = repo @@ -662,7 +654,7 @@ func NotifyWatchers(ctx context.Context, actions ...*Action) ([]Action, error) { act.ID = 0 act.UserID = act.Repo.Owner.ID if err = db.Insert(ctx, act); err != nil { - return nil, fmt.Errorf("insert new actioner: %w", err) + return fmt.Errorf("insert new actioner: %w", err) } } @@ -715,29 +707,26 @@ func NotifyWatchers(ctx context.Context, actions ...*Action) ([]Action, error) { } if err = db.Insert(ctx, act); err != nil { - return nil, fmt.Errorf("insert new action: %w", err) + return fmt.Errorf("insert new action: %w", err) } } } - return out, nil + return nil } // NotifyWatchersActions creates batch of actions for every watcher. -func NotifyWatchersActions(ctx context.Context, acts []*Action) ([]Action, error) { +func NotifyWatchersActions(ctx context.Context, acts []*Action) error { ctx, committer, err := db.TxContext(ctx) if err != nil { - return nil, err + return err } defer committer.Close() - var out []Action for _, act := range acts { - as, err := NotifyWatchers(ctx, act) - if err != nil { - return nil, err + if err := NotifyWatchers(ctx, act); err != nil { + return err } - out = append(out, as...) } - return out, committer.Commit() + return committer.Commit() } // DeleteIssueActions delete all actions related with issueID diff --git a/models/activities/action_test.go b/models/activities/action_test.go index 47dbd8ac2d..bcc9c98cec 100644 --- a/models/activities/action_test.go +++ b/models/activities/action_test.go @@ -197,8 +197,7 @@ func TestNotifyWatchers(t *testing.T) { RepoID: 1, OpType: activities_model.ActionStarRepo, } - _, err := activities_model.NotifyWatchers(db.DefaultContext, action) - require.NoError(t, err) + require.NoError(t, activities_model.NotifyWatchers(db.DefaultContext, action)) // One watchers are inactive, thus action is only created for user 8, 1, 4, 11 unittest.AssertExistsAndLoadBean(t, &activities_model.Action{ diff --git a/models/activities/federated_user_activity.go b/models/activities/federated_user_activity.go deleted file mode 100644 index 1ff3a855d0..0000000000 --- a/models/activities/federated_user_activity.go +++ /dev/null @@ -1,106 +0,0 @@ -// Copyright 2024 The Forgejo Authors. All rights reserved. -// SPDX-License-Identifier: MIT - -package activities - -import ( - "context" - "fmt" - - "forgejo.org/models/db" - user_model "forgejo.org/models/user" - "forgejo.org/modules/json" - "forgejo.org/modules/log" - "forgejo.org/modules/timeutil" - "forgejo.org/modules/validation" - - ap "github.com/go-ap/activitypub" -) - -type FederatedUserActivity struct { - ID int64 `xorm:"pk autoincr"` - UserID int64 `xorm:"NOT NULL"` - ActorID int64 - ActorURI string - Actor *user_model.User `xorm:"-"` // transient - NoteContent string `xorm:"TEXT"` - NoteURL string `xorm:"VARCHAR(255)"` - OriginalNote string `xorm:"TEXT"` - Created timeutil.TimeStamp `xorm:"created"` -} - -func init() { - db.RegisterModel(new(FederatedUserActivity)) -} - -func NewFederatedUserActivity(userID, actorID int64, actorURI, noteContent, noteURL string, originalNote ap.Activity) (FederatedUserActivity, error) { - jsonString, err := json.Marshal(originalNote) - if err != nil { - return FederatedUserActivity{}, err - } - result := FederatedUserActivity{ - UserID: userID, - ActorID: actorID, - ActorURI: actorURI, - NoteContent: noteContent, - NoteURL: noteURL, - OriginalNote: string(jsonString), - } - if valid, err := validation.IsValid(result); !valid { - return FederatedUserActivity{}, err - } - return result, nil -} - -func (federatedUserActivity FederatedUserActivity) Validate() []string { - var result []string - result = append(result, validation.ValidateNotEmpty(federatedUserActivity.UserID, "UserID")...) - result = append(result, validation.ValidateNotEmpty(federatedUserActivity.ActorID, "ActorID")...) - result = append(result, validation.ValidateNotEmpty(federatedUserActivity.ActorURI, "ActorURI")...) - result = append(result, validation.ValidateNotEmpty(federatedUserActivity.NoteContent, "NoteContent")...) - result = append(result, validation.ValidateNotEmpty(federatedUserActivity.NoteURL, "NoteURL")...) - result = append(result, validation.ValidateNotEmpty(federatedUserActivity.OriginalNote, "OriginalNote")...) - return result -} - -func CreateUserActivity(ctx context.Context, federatedUserActivity *FederatedUserActivity) error { - if valid, err := validation.IsValid(federatedUserActivity); !valid { - return err - } - _, err := db.GetEngine(ctx).Insert(federatedUserActivity) - return err -} - -type GetFollowingFeedsOptions struct { - db.ListOptions -} - -func GetFollowingFeeds(ctx context.Context, actorID int64, opts GetFollowingFeedsOptions) ([]*FederatedUserActivity, int64, error) { - log.Debug("user_id = %s", actorID) - sess := db.GetEngine(ctx).Where("user_id = ?", actorID) - opts.SetDefaultValues() - sess = db.SetSessionPagination(sess, &opts) - - actions := make([]*FederatedUserActivity, 0, opts.PageSize) - count, err := sess.FindAndCount(&actions) - if err != nil { - return nil, 0, fmt.Errorf("FindAndCount: %w", err) - } - for _, act := range actions { - if err := act.loadActor(ctx); err != nil { - return nil, 0, err - } - } - return actions, count, err -} - -func (federatedUserActivity *FederatedUserActivity) loadActor(ctx context.Context) error { - log.Debug("for activity %s", federatedUserActivity) - actorUser, _, err := user_model.GetFederatedUserByUserID(ctx, federatedUserActivity.ActorID) - if err != nil { - return err - } - federatedUserActivity.Actor = actorUser - - return nil -} diff --git a/models/activities/federated_user_activity_test.go b/models/activities/federated_user_activity_test.go deleted file mode 100644 index 9bf4f77984..0000000000 --- a/models/activities/federated_user_activity_test.go +++ /dev/null @@ -1,24 +0,0 @@ -// Copyright 2024 The Forgejo Authors. All rights reserved. -// SPDX-License-Identifier: MIT - -package activities - -import ( - "testing" - - "forgejo.org/modules/validation" -) - -func Test_FederatedUserActivityValidation(t *testing.T) { - sut := FederatedUserActivity{} - sut.UserID = 13 - sut.ActorID = 33 - sut.ActorURI = "33" - sut.NoteContent = "Any content!" - sut.NoteURL = "https://example.org/note/17" - sut.OriginalNote = "federatedUserActivityNote-17" - - if res, _ := validation.IsValid(sut); !res { - t.Errorf("sut expected to be valid: %v\n", sut.Validate()) - } -} diff --git a/models/fixtures/comment.yml b/models/fixtures/comment.yml index 34407d6f81..2c47196c05 100644 --- a/models/fixtures/comment.yml +++ b/models/fixtures/comment.yml @@ -153,304 +153,3 @@ issue_id: 19 # in repo_id 58 content: '{"is_force_push":true,"commit_ids":["1978192d98bb1b65e11c2cf37da854fbf94bffd6", "9b93963cf6de4dc33f915bb67f192d099c301f43"]}' created_unix: 1749734240 - -- - id: 2000 - type: 8 # milestone - poster_id: 1 - issue_id: 1 # in repo_id 1 - milestone_id: 1 - old_milestone_id: 0 - created_unix: 946684820 - -- - id: 2001 - type: 8 # milestone - poster_id: 1 - issue_id: 1 # in repo_id 1 - milestone_id: 2 - old_milestone_id: 1 - created_unix: 946684920 - -- - id: 2002 - type: 8 # milestone - poster_id: 1 - issue_id: 1 # in repo_id 1 - milestone_id: 0 - old_milestone_id: 2 - created_unix: 946685020 - -- - id: 2003 - type: 8 # milestone - poster_id: 1 - issue_id: 1 # in repo_id 1 - milestone_id: 10 # not exsting milestone - old_milestone_id: 0 - created_unix: 946685080 - -- - id: 2010 - type: 30 # project - poster_id: 1 - issue_id: 1 # in repo_id 1 - project_id: 1 - old_project_id: 0 - created_unix: 946685120 - -- - id: 2011 - type: 30 # project - poster_id: 1 - issue_id: 1 # in repo_id 1 - project_id: 2 - old_project_id: 1 - created_unix: 946685220 - -- - id: 2012 - type: 30 # project - poster_id: 1 - issue_id: 1 # in repo_id 1 - project_id: 0 - old_project_id: 2 - created_unix: 946685320 - -- - id: 2013 - type: 30 # project - poster_id: 1 - issue_id: 1 # in repo_id 1 - project_id: 10 # not existing project - old_project_id: 0 - created_unix: 946685420 - -- - id: 2020 - type: 7 # label - poster_id: 1 - issue_id: 1 # in repo_id 1 - label_id: 1 - content: 1 # add label - created_unix: 946685520 - -- - id: 2021 - type: 7 # label - poster_id: 1 - issue_id: 1 - label_id: 2 - content: 1 # add label - created_unix: 946685620 - -- - id: 2022 - type: 7 # label - poster_id: 2 - issue_id: 1 # in repo_id 1 - label_id: 1 - content: 0 # remove label - created_unix: 946685720 - -- - id: 2023 - type: 7 # label - poster_id: 1 - issue_id: 1 # in repo_id 1 - label_id: 1 - content: 1 # add label - created_unix: 946685720 - -- - id: 2024 - type: 7 # label - poster_id: 1 - issue_id: 1 # in repo_id 1 - label_id: 2 - content: 0 # remove label - created_unix: 946685720 - -- - id: 2025 - type: 7 # label - poster_id: 2 - issue_id: 1 # in repo_id 1 - label_id: 2 - content: 1 # add label - created_unix: 946685820 - -- - id: 2026 - type: 7 # label - poster_id: 1 - issue_id: 1 # in repo_id 1 - label_id: 1 - content: 0 # remove label - created_unix: 946685920 - -- - id: 2027 - type: 7 # label - poster_id: 1 - issue_id: 1 # in repo_id 1 - label_id: 2 - content: 0 # remove label - created_unix: 946685920 - -- id: 2040 - type: 9 # assignee - poster_id: 1 - issue_id: 1 # in repo_id 1 - assignee_id: 1 # self - removed_assignee: false # add assignee - created_unix: 946688020 - -- id: 2041 - type: 9 # assignee - poster_id: 2 - issue_id: 1 # in repo_id 1 - assignee_id: 1 - removed_assignee: true # remove assignee - created_unix: 946688120 - -- id: 2042 - type: 9 # assignee - poster_id: 1 - issue_id: 1 # in repo_id 1 - assignee_id: 2 - removed_assignee: false # add assignee - created_unix: 946688220 - -- id: 2043 - type: 9 # assignee - poster_id: 2 - issue_id: 1 # in repo_id 1 - assignee_id: 2 # self - removed_assignee: true # remove assignee - created_unix: 946688320 - -- id: 2050 - type: 23 # lock - poster_id: 1 - issue_id: 1 # in repo_id 1 - created_unix: 946688420 - -- id: 2051 - type: 24 # unlock - poster_id: 1 - issue_id: 1 # in repo_id 1 - created_unix: 946688520 - -- id: 2052 - type: 23 # lock - poster_id: 1 - issue_id: 1 # in repo_id 1 - content: "Too heated" - created_unix: 946688620 - -- id: 2053 - type: 24 # unlock - poster_id: 1 - issue_id: 1 # in repo_id 1 - created_unix: 946688720 - -- id: 2060 - type: 36 # pin - poster_id: 1 - issue_id: 1 # in repo_id 1 - created_unix: 946688820 - -- id: 2061 - type: 37 # unpin - poster_id: 1 - issue_id: 1 # in repo_id 1 - created_unix: 946688920 - -- id: 2070 - type: 2 # close - poster_id: 1 - issue_id: 1 # in repo_id 1 - created_unix: 946689020 - -- id: 2071 - type: 1 # reopen - poster_id: 2 - issue_id: 1 # in repo_id 1 - created_unix: 946689220 - -- id: 2072 - type: 2 # close - poster_id: 1 - issue_id: 2 # pull in repo_id 1 - created_unix: 946689320 - -- id: 2073 - type: 1 # reopen - poster_id: 2 - issue_id: 2 # pull in repo_id 1 - created_unix: 946689420 - -- id: 2080 - type: 3 # issue reference - poster_id: 1 - issue_id: 1 # issue in repo_id 1 - ref_repo_id: 1 - ref_issue_id: 5 # issue in repo_id 1 - created_unix: 946689500 - -- id: 2081 - type: 3 # issue reference - poster_id: 1 - issue_id: 1 # issue in repo_id 1 - ref_repo_id: 1 - ref_issue_id: 2 # pull in repo_id 1 - created_unix: 946689600 - -- id: 2082 - type: 3 # issue reference - poster_id: 1 - issue_id: 1 # issue in repo_id 1 - ref_repo_id: 32 - ref_issue_id: 16 # issue in repo_id 32 - created_unix: 946689700 - -- id: 2083 - type: 3 # issue reference - poster_id: 1 - issue_id: 1 # issue in repo_id 1 - ref_repo_id: 10 - ref_issue_id: 8 # pull in repo_id 10 - created_unix: 946689800 - -- id: 2090 - type: 6 # pull reference - poster_id: 1 - issue_id: 2 # pull in repo_id 1 - ref_repo_id: 1 - ref_issue_id: 1 # issue in repo_id 1 - created_unix: 946689900 - -- id: 2091 - type: 6 # pull reference - poster_id: 1 - issue_id: 2 # pull in repo_id 1 - ref_repo_id: 1 - ref_issue_id: 2 # pull in repo_id 1 - created_unix: 946690000 - -- id: 2092 - type: 6 # pull reference - poster_id: 1 - issue_id: 2 # pull in repo_id 1 - ref_repo_id: 32 - ref_issue_id: 16 # issue in repo_id 32 - created_unix: 946690050 - -- id: 2093 - type: 6 # pull reference - poster_id: 1 - issue_id: 2 # pull in repo_id 1 - ref_repo_id: 10 - ref_issue_id: 8 # pull in repo_id 10 - created_unix: 946690100 diff --git a/models/forgefed/federationhost.go b/models/forgefed/federationhost.go index 978847bd95..29f1b7d28e 100644 --- a/models/forgefed/federationhost.go +++ b/models/forgefed/federationhost.go @@ -6,7 +6,6 @@ package forgefed import ( "database/sql" "fmt" - "net/url" "strings" "time" @@ -18,9 +17,9 @@ import ( // swagger:model type FederationHost struct { ID int64 `xorm:"pk autoincr"` - HostFqdn string `xorm:"host_fqdn UNIQUE(federation_host) INDEX VARCHAR(255) NOT NULL"` - HostPort uint16 `xorm:" UNIQUE(federation_host) INDEX NOT NULL DEFAULT 443"` + HostFqdn string `xorm:"host_fqdn UNIQUE INDEX VARCHAR(255) NOT NULL"` NodeInfo NodeInfo `xorm:"extends NOT NULL"` + HostPort uint16 `xorm:"NOT NULL DEFAULT 443"` HostSchema string `xorm:"NOT NULL DEFAULT 'https'"` LatestActivity time.Time `xorm:"NOT NULL"` KeyID sql.NullString `xorm:"key_id UNIQUE"` @@ -43,13 +42,6 @@ func NewFederationHost(hostFqdn string, nodeInfo NodeInfo, port uint16, schema s return result, nil } -func (host FederationHost) AsURL() url.URL { - return url.URL{ - Scheme: host.HostSchema, - Host: fmt.Sprintf("%v:%v", host.HostFqdn, host.HostPort), - } -} - // Validate collects error strings in a slice and returns this func (host FederationHost) Validate() []string { var result []string diff --git a/models/forgefed/nodeinfo.go b/models/forgefed/nodeinfo.go index 38f51304c5..2461b5e499 100644 --- a/models/forgefed/nodeinfo.go +++ b/models/forgefed/nodeinfo.go @@ -17,14 +17,12 @@ type ( ) const ( - ForgejoSourceType SoftwareNameType = "forgejo" - GiteaSourceType SoftwareNameType = "gitea" - MastodonSourceType SoftwareNameType = "mastodon" - GoToSocialSourceType SoftwareNameType = "gotosocial" + ForgejoSourceType SoftwareNameType = "forgejo" + GiteaSourceType SoftwareNameType = "gitea" ) var KnownSourceTypes = []any{ - ForgejoSourceType, GiteaSourceType, MastodonSourceType, GoToSocialSourceType, + ForgejoSourceType, GiteaSourceType, } // ------------------------------------------------ NodeInfoWellKnown ------------------------------------------------ diff --git a/models/forgejo_migrations/migrate.go b/models/forgejo_migrations/migrate.go index 737350b019..21a2077d06 100644 --- a/models/forgejo_migrations/migrate.go +++ b/models/forgejo_migrations/migrate.go @@ -103,12 +103,6 @@ var migrations = []*Migration{ NewMigration("Normalize repository.topics to empty slice instead of null", SetTopicsAsEmptySlice), // v31 -> v32 NewMigration("Migrate maven package name concatenation", ChangeMavenArtifactConcatenation), - // v32 -> v33 - NewMigration("Add federated user activity tables, update the `federated_user` table & add indexes", FederatedUserActivityMigration), - // v33 -> v34 - NewMigration("Add `notify-email` column to `action_run` table", AddNotifyEmailToActionRun), - // v34 -> v35 - NewMigration("Add index to `stopped` column in `action_run` table", AddIndexToActionRunStopped), } // GetCurrentDBVersion returns the current Forgejo database version. diff --git a/models/forgejo_migrations/v33.go b/models/forgejo_migrations/v33.go deleted file mode 100644 index 272035fc23..0000000000 --- a/models/forgejo_migrations/v33.go +++ /dev/null @@ -1,126 +0,0 @@ -// Copyright 2025 The Forgejo Authors. All rights reserved. -// SPDX-License-Identifier: MIT - -package forgejo_migrations //nolint:revive - -import ( - "fmt" - - "forgejo.org/modules/log" - "forgejo.org/modules/timeutil" - - "xorm.io/xorm" -) - -func dropOldFederationHostIndexes(x *xorm.Engine) { - // drop unique index on HostFqdn - type FederationHost struct { - ID int64 `xorm:"pk autoincr"` - HostFqdn string `xorm:"host_fqdn UNIQUE INDEX VARCHAR(255) NOT NULL"` - } - - err := x.DropIndexes(FederationHost{}) - if err != nil { - log.Warn("migration[33]: There was an issue: %v", err) - return - } -} - -func addFederatedUserActivityTables(x *xorm.Engine) { - type FederatedUserActivity struct { - ID int64 `xorm:"pk autoincr"` - UserID int64 `xorm:"NOT NULL INDEX user_id"` - ActorID int64 - ActorURI string - NoteContent string `xorm:"TEXT"` - NoteURL string `xorm:"VARCHAR(255)"` - OriginalNote string `xorm:"TEXT"` - Created timeutil.TimeStamp `xorm:"created"` - } - - // add unique index on HostFqdn+HostPort - type FederationHost struct { - ID int64 `xorm:"pk autoincr"` - HostFqdn string `xorm:"host_fqdn UNIQUE(federation_host) INDEX VARCHAR(255) NOT NULL"` - HostPort uint16 `xorm:"UNIQUE(federation_host) INDEX NOT NULL DEFAULT 443"` - } - - type FederatedUserFollower struct { - ID int64 `xorm:"pk autoincr"` - - FollowedUserID int64 `xorm:"NOT NULL unique(fuf_rel)"` - FollowingUserID int64 `xorm:"NOT NULL unique(fuf_rel)"` - } - - // Add InboxPath to FederatedUser & add index fo UserID - type FederatedUser struct { - ID int64 `xorm:"pk autoincr"` - UserID int64 `xorm:"NOT NULL INDEX user_id"` - InboxPath string - } - - var err error - - err = x.Sync(&FederationHost{}) - if err != nil { - log.Warn("migration[33]: There was an issue: %v", err) - return - } - - err = x.Sync(&FederatedUserActivity{}) - if err != nil { - log.Warn("migration[33]: There was an issue: %v", err) - return - } - - err = x.Sync(&FederatedUserFollower{}) - if err != nil { - log.Warn("migration[33]: There was an issue: %v", err) - return - } - - err = x.Sync(&FederatedUser{}) - if err != nil { - log.Warn("migration[33]: There was an issue: %v", err) - return - } - - // Migrate - sessMigration := x.NewSession() - defer sessMigration.Close() - if err := sessMigration.Begin(); err != nil { - log.Warn("migration[33]: There was an issue: %v", err) - return - } - federatedUsers := make([]*FederatedUser, 0) - err = sessMigration.OrderBy("id").Find(&federatedUsers) - if err != nil { - log.Warn("migration[33]: There was an issue: %v", err) - return - } - - for _, federatedUser := range federatedUsers { - if federatedUser.InboxPath != "" { - log.Info("migration[33]: This user was already migrated: %v", federatedUser) - } else { - // Migrate User.InboxPath - sql := "UPDATE `federated_user` SET `inbox_path` = ? WHERE `id` = ?" - if _, err := sessMigration.Exec(sql, fmt.Sprintf("/api/v1/activitypub/user-id/%v/inbox", federatedUser.UserID), federatedUser.ID); err != nil { - log.Warn("migration[33]: There was an issue: %v", err) - return - } - } - } - - err = sessMigration.Commit() - if err != nil { - log.Warn("migration[33]: There was an issue: %v", err) - return - } -} - -func FederatedUserActivityMigration(x *xorm.Engine) error { - dropOldFederationHostIndexes(x) - addFederatedUserActivityTables(x) - return nil -} diff --git a/models/forgejo_migrations/v33_test.go b/models/forgejo_migrations/v33_test.go deleted file mode 100644 index 664c704bbc..0000000000 --- a/models/forgejo_migrations/v33_test.go +++ /dev/null @@ -1,46 +0,0 @@ -// Copyright 2025 The Forgejo Authors. -// SPDX-License-Identifier: GPL-3.0-or-later - -package forgejo_migrations //nolint:revive - -import ( - "testing" - "time" - - migration_tests "forgejo.org/models/migrations/test" - "forgejo.org/modules/log" - ft "forgejo.org/modules/test" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -func Test_FederatedUserActivityMigration(t *testing.T) { - lc, cl := ft.NewLogChecker(log.DEFAULT, log.WARN) - lc.Filter("migration[33]") - defer cl() - - // intentionally conflicting definition - type FederatedUser struct { - ID int64 `xorm:"pk autoincr"` - UserID string - } - - // Prepare TestEnv - x, deferable := migration_tests.PrepareTestEnv(t, 0, - new(FederatedUser), - ) - sessTest := x.NewSession() - sessTest.Insert(FederatedUser{UserID: "1234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890" + - "1234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890" + - "1234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890"}) - sessTest.Commit() - defer deferable() - if x == nil || t.Failed() { - return - } - - require.NoError(t, FederatedUserActivityMigration(x)) - logFiltered, _ := lc.Check(5 * time.Second) - assert.NotEmpty(t, logFiltered) -} diff --git a/models/forgejo_migrations/v34.go b/models/forgejo_migrations/v34.go deleted file mode 100644 index 9e958b934f..0000000000 --- a/models/forgejo_migrations/v34.go +++ /dev/null @@ -1,14 +0,0 @@ -// Copyright 2025 The Forgejo Authors. All rights reserved. -// SPDX-License-Identifier: GPL-3.0-or-later - -package forgejo_migrations //nolint:revive - -import "xorm.io/xorm" - -func AddNotifyEmailToActionRun(x *xorm.Engine) error { - type ActionRun struct { - ID int64 - NotifyEmail bool - } - return x.Sync(new(ActionRun)) -} diff --git a/models/forgejo_migrations/v35.go b/models/forgejo_migrations/v35.go deleted file mode 100644 index 0fb3b43e2c..0000000000 --- a/models/forgejo_migrations/v35.go +++ /dev/null @@ -1,19 +0,0 @@ -// Copyright 2025 The Forgejo Authors. All rights reserved. -// SPDX-License-Identifier: GPL-3.0-or-later - -package forgejo_migrations //nolint:revive - -import ( - "forgejo.org/modules/timeutil" - - "xorm.io/xorm" -) - -func AddIndexToActionRunStopped(x *xorm.Engine) error { - type ActionRun struct { - ID int64 - Stopped timeutil.TimeStamp `xorm:"index"` - } - - return x.Sync(&ActionRun{}) -} diff --git a/models/issues/pull.go b/models/issues/pull.go index 188ef00814..0781fd0a2d 100644 --- a/models/issues/pull.go +++ b/models/issues/pull.go @@ -5,7 +5,6 @@ package issues import ( - "bufio" "context" "errors" "fmt" @@ -924,30 +923,31 @@ func MergeBlockedByOutdatedBranch(protectBranch *git_model.ProtectedBranch, pr * return protectBranch.BlockOnOutdatedBranch && pr.CommitsBehind > 0 } -// GetCodeOwnersFromReader returns the code owners configuration +// GetCodeOwnersFromContent returns the code owners configuration +// Return empty slice if files missing // Return warning messages on parsing errors // We're trying to do the best we can when parsing a file. // Invalid lines are skipped. Non-existent users and teams too. -func GetCodeOwnersFromReader(ctx context.Context, rc io.ReadCloser, truncated bool) ([]*CodeOwnerRule, []string) { - defer rc.Close() - scanner := bufio.NewScanner(rc) +func GetCodeOwnersFromContent(ctx context.Context, data string) ([]*CodeOwnerRule, []string) { + if len(data) == 0 { + return nil, nil + } - var rules []*CodeOwnerRule - var warnings []string - line := 0 - for scanner.Scan() { - line++ + rules := make([]*CodeOwnerRule, 0) + lines := strings.Split(data, "\n") + warnings := make([]string, 0) - tokens := TokenizeCodeOwnersLine(scanner.Text()) + for i, line := range lines { + tokens := TokenizeCodeOwnersLine(line) if len(tokens) == 0 { continue } else if len(tokens) < 2 { - warnings = append(warnings, fmt.Sprintf("Line: %d: incorrect format", line)) + warnings = append(warnings, fmt.Sprintf("Line: %d: incorrect format", i+1)) continue } rule, wr := ParseCodeOwnersLine(ctx, tokens) for _, w := range wr { - warnings = append(warnings, fmt.Sprintf("Line: %d: %s", line, w)) + warnings = append(warnings, fmt.Sprintf("Line: %d: %s", i+1, w)) } if rule == nil { continue @@ -955,12 +955,6 @@ func GetCodeOwnersFromReader(ctx context.Context, rc io.ReadCloser, truncated bo rules = append(rules, rule) } - if err := scanner.Err(); err != nil { - warnings = append(warnings, err.Error()) - } - if truncated { - warnings = append(warnings, fmt.Sprintf("File too big: truncated while on line %d", line)) - } return rules, warnings } diff --git a/models/user/federated_user.go b/models/user/federated_user.go index d2a9c34c9e..c1833c7de3 100644 --- a/models/user/federated_user.go +++ b/models/user/federated_user.go @@ -11,21 +11,19 @@ import ( type FederatedUser struct { ID int64 `xorm:"pk autoincr"` - UserID int64 `xorm:"NOT NULL INDEX user_id"` + UserID int64 `xorm:"NOT NULL"` ExternalID string `xorm:"UNIQUE(federation_user_mapping) NOT NULL"` FederationHostID int64 `xorm:"UNIQUE(federation_user_mapping) NOT NULL"` KeyID sql.NullString `xorm:"key_id UNIQUE"` PublicKey sql.Null[sql.RawBytes] `xorm:"BLOB"` - InboxPath string - NormalizedOriginalURL string // This field is just to keep original information. Pls. do not use for search or as ID! + NormalizedOriginalURL string // This field is just to keep original information. Pls. do not use for search or as ID! } -func NewFederatedUser(userID int64, externalID string, federationHostID int64, inboxPath, normalizedOriginalURL string) (FederatedUser, error) { +func NewFederatedUser(userID int64, externalID string, federationHostID int64, normalizedOriginalURL string) (FederatedUser, error) { result := FederatedUser{ UserID: userID, ExternalID: externalID, FederationHostID: federationHostID, - InboxPath: inboxPath, NormalizedOriginalURL: normalizedOriginalURL, } if valid, err := validation.IsValid(result); !valid { @@ -34,11 +32,10 @@ func NewFederatedUser(userID int64, externalID string, federationHostID int64, i return result, nil } -func (federatedUser FederatedUser) Validate() []string { +func (user FederatedUser) Validate() []string { var result []string - result = append(result, validation.ValidateNotEmpty(federatedUser.UserID, "UserID")...) - result = append(result, validation.ValidateNotEmpty(federatedUser.ExternalID, "ExternalID")...) - result = append(result, validation.ValidateNotEmpty(federatedUser.FederationHostID, "FederationHostID")...) - result = append(result, validation.ValidateNotEmpty(federatedUser.InboxPath, "InboxPath")...) + result = append(result, validation.ValidateNotEmpty(user.UserID, "UserID")...) + result = append(result, validation.ValidateNotEmpty(user.ExternalID, "ExternalID")...) + result = append(result, validation.ValidateNotEmpty(user.FederationHostID, "FederationHostID")...) return result } diff --git a/models/user/federated_user_follower.go b/models/user/federated_user_follower.go deleted file mode 100644 index db72c9b5ce..0000000000 --- a/models/user/federated_user_follower.go +++ /dev/null @@ -1,30 +0,0 @@ -// Copyright 2025 The Forgejo Authors. All rights reserved. -// SPDX-License-Identifier: MIT - -package user - -import "forgejo.org/modules/validation" - -type FederatedUserFollower struct { - ID int64 `xorm:"pk autoincr"` - FollowedUserID int64 `xorm:"NOT NULL unique(fuf_rel)"` - FollowingUserID int64 `xorm:"NOT NULL unique(fuf_rel)"` -} - -func NewFederatedUserFollower(followedUserID, federatedUserID int64) (FederatedUserFollower, error) { - result := FederatedUserFollower{ - FollowedUserID: followedUserID, - FollowingUserID: federatedUserID, - } - if valid, err := validation.IsValid(result); !valid { - return FederatedUserFollower{}, err - } - return result, nil -} - -func (user FederatedUserFollower) Validate() []string { - var result []string - result = append(result, validation.ValidateNotEmpty(user.FollowedUserID, "FollowedUserID")...) - result = append(result, validation.ValidateNotEmpty(user.FollowingUserID, "FollowingUserID")...) - return result -} diff --git a/models/user/federated_user_follower_test.go b/models/user/federated_user_follower_test.go deleted file mode 100644 index e57ba01308..0000000000 --- a/models/user/federated_user_follower_test.go +++ /dev/null @@ -1,27 +0,0 @@ -// Copyright 2024 The Forgejo Authors. All rights reserved. -// SPDX-License-Identifier: MIT - -package user - -import ( - "testing" - - "forgejo.org/modules/validation" - - "github.com/stretchr/testify/assert" -) - -func Test_FederatedUserFollowerValidation(t *testing.T) { - sut := FederatedUserFollower{ - FollowedUserID: 12, - FollowingUserID: 1, - } - res, err := validation.IsValid(sut) - assert.Truef(t, res, "sut should be valid but was %q", err) - - sut = FederatedUserFollower{ - FollowedUserID: 1, - } - res, _ = validation.IsValid(sut) - assert.False(t, res, "sut should be invalid") -} diff --git a/models/user/federated_user_test.go b/models/user/federated_user_test.go index be18339670..542798c9bc 100644 --- a/models/user/federated_user_test.go +++ b/models/user/federated_user_test.go @@ -14,7 +14,6 @@ func Test_FederatedUserValidation(t *testing.T) { UserID: 12, ExternalID: "12", FederationHostID: 1, - InboxPath: "/api/v1/activitypub/user-id/12/inbox", } if res, err := validation.IsValid(sut); !res { t.Errorf("sut should be valid but was %q", err) @@ -23,7 +22,6 @@ func Test_FederatedUserValidation(t *testing.T) { sut = FederatedUser{ ExternalID: "12", FederationHostID: 1, - InboxPath: "/api/v1/activitypub/user-id/12/inbox", } if res, _ := validation.IsValid(sut); res { t.Error("sut should be invalid") diff --git a/models/user/follow.go b/models/user/follow.go index e32c226385..5be0f73c35 100644 --- a/models/user/follow.go +++ b/models/user/follow.go @@ -11,7 +11,6 @@ import ( ) // Follow represents relations of user and their followers. -// TODO: We should unify Activity-pub-following and classical following (see models/user/user_repository.go#IsFollowingAp) type Follow struct { ID int64 `xorm:"pk autoincr"` UserID int64 `xorm:"UNIQUE(follow)"` diff --git a/models/user/user_repository.go b/models/user/user_repository.go index 3f24efb1fb..299d3af64a 100644 --- a/models/user/user_repository.go +++ b/models/user/user_repository.go @@ -1,4 +1,4 @@ -// Copyright 2024, 2025 The Forgejo Authors. All rights reserved. +// Copyright 2024 The Forgejo Authors. All rights reserved. // SPDX-License-Identifier: MIT package user @@ -8,14 +8,12 @@ import ( "fmt" "forgejo.org/models/db" - "forgejo.org/modules/log" "forgejo.org/modules/optional" "forgejo.org/modules/validation" ) func init() { db.RegisterModel(new(FederatedUser)) - db.RegisterModel(new(FederatedUserFollower)) } func CreateFederatedUser(ctx context.Context, user *User, federatedUser *FederatedUser) error { @@ -32,12 +30,7 @@ func CreateFederatedUser(ctx context.Context, user *User, federatedUser *Federat if err != nil { return err } - defer func() { - err := committer.Close() - if err != nil { - log.Error("Error closing committer: %v", err) - } - }() + defer committer.Close() if err := CreateUser(ctx, user, &overwrite); err != nil { return err @@ -57,14 +50,6 @@ func CreateFederatedUser(ctx context.Context, user *User, federatedUser *Federat return committer.Commit() } -func (federatedUser *FederatedUser) UpdateFederatedUser(ctx context.Context) error { - if _, err := validation.IsValid(federatedUser); err != nil { - return err - } - _, err := db.GetEngine(ctx).ID(federatedUser.ID).Cols("inbox_path").Update(federatedUser) - return err -} - func FindFederatedUser(ctx context.Context, externalID string, federationHostID int64) (*User, *FederatedUser, error) { federatedUser := new(FederatedUser) user := new(User) @@ -90,41 +75,6 @@ func FindFederatedUser(ctx context.Context, externalID string, federationHostID return user, federatedUser, nil } -func GetFederatedUser(ctx context.Context, externalID string, federationHostID int64) (*User, *FederatedUser, error) { - user, federatedUser, err := FindFederatedUser(ctx, externalID, federationHostID) - if err != nil { - return nil, nil, err - } else if federatedUser == nil { - return nil, nil, fmt.Errorf("FederatedUser for externalId = %v and federationHostId = %v does not exist", externalID, federationHostID) - } - return user, federatedUser, nil -} - -func GetFederatedUserByUserID(ctx context.Context, userID int64) (*User, *FederatedUser, error) { - federatedUser := new(FederatedUser) - user := new(User) - has, err := db.GetEngine(ctx).Where("user_id=?", userID).Get(federatedUser) - if err != nil { - return nil, nil, err - } else if !has { - return nil, nil, fmt.Errorf("Federated user %v does not exist", federatedUser.UserID) - } - has, err = db.GetEngine(ctx).ID(federatedUser.UserID).Get(user) - if err != nil { - return nil, nil, err - } else if !has { - return nil, nil, fmt.Errorf("User %v for federated user is missing", federatedUser.UserID) - } - - if res, err := validation.IsValid(*user); !res { - return nil, nil, err - } - if res, err := validation.IsValid(*federatedUser); !res { - return nil, nil, err - } - return user, federatedUser, nil -} - func FindFederatedUserByKeyID(ctx context.Context, keyID string) (*User, *FederatedUser, error) { federatedUser := new(FederatedUser) user := new(User) @@ -151,85 +101,7 @@ func FindFederatedUserByKeyID(ctx context.Context, keyID string) (*User, *Federa return user, federatedUser, nil } -func UpdateFederatedUser(ctx context.Context, federatedUser *FederatedUser) error { - if res, err := validation.IsValid(federatedUser); !res { - return err - } - _, err := db.GetEngine(ctx).ID(federatedUser.ID).Update(federatedUser) - return err -} - func DeleteFederatedUser(ctx context.Context, userID int64) error { _, err := db.GetEngine(ctx).Delete(&FederatedUser{UserID: userID}) return err } - -func GetFollowersForUser(ctx context.Context, user *User) ([]*FederatedUserFollower, error) { - if res, err := validation.IsValid(user); !res { - return nil, err - } - followers := make([]*FederatedUserFollower, 0, 8) - - err := db.GetEngine(ctx). - Where("followed_user_id = ?", user.ID). - Find(&followers) - if err != nil { - return nil, err - } - for _, element := range followers { - if res, err := validation.IsValid(*element); !res { - return nil, err - } - } - return followers, nil -} - -func AddFollower(ctx context.Context, followedUser *User, followingUser *FederatedUser) (*FederatedUserFollower, error) { - if res, err := validation.IsValid(followedUser); !res { - return nil, err - } - if res, err := validation.IsValid(followingUser); !res { - return nil, err - } - - federatedUserFollower, err := NewFederatedUserFollower(followedUser.ID, followingUser.UserID) - if err != nil { - return nil, err - } - _, err = db.GetEngine(ctx).Insert(&federatedUserFollower) - if err != nil { - return nil, err - } - - return &federatedUserFollower, err -} - -func RemoveFollower(ctx context.Context, followedUser *User, followingUser *FederatedUser) error { - if res, err := validation.IsValid(followedUser); !res { - return err - } - if res, err := validation.IsValid(followingUser); !res { - return err - } - - _, err := db.GetEngine(ctx).Delete(&FederatedUserFollower{ - FollowedUserID: followedUser.ID, - FollowingUserID: followingUser.UserID, - }) - return err -} - -// TODO: We should unify Activity-pub-following and classical following (see models/user/follow.go) -func IsFollowingAp(ctx context.Context, followedUser *User, followingUser *FederatedUser) (bool, error) { - if res, err := validation.IsValid(followedUser); !res { - return false, err - } - if res, err := validation.IsValid(followingUser); !res { - return false, err - } - - return db.GetEngine(ctx).Get(&FederatedUserFollower{ - FollowedUserID: followedUser.ID, - FollowingUserID: followingUser.UserID, - }) -} diff --git a/models/user/user_system.go b/models/user/user_system.go index 11f54591b7..82805cc8ee 100644 --- a/models/user/user_system.go +++ b/models/user/user_system.go @@ -12,13 +12,6 @@ import ( "forgejo.org/modules/structs" ) -// IsSystem returns true if the user has a fixed -// negative ID, is never stored in the database and -// is generated on the fly when needed. -func (u *User) IsSystem() bool { - return u.IsGhost() || u.IsActions() -} - const ( GhostUserID = -1 GhostUserName = "Ghost" diff --git a/models/user/user_test.go b/models/user/user_test.go index fd9d05653f..2a9e652a35 100644 --- a/models/user/user_test.go +++ b/models/user/user_test.go @@ -148,7 +148,7 @@ func TestAPActorID_APActorID(t *testing.T) { assert.Equal(t, expected, url) } -func TestKeyID(t *testing.T) { +func TestAPActorKeyID(t *testing.T) { user := user_model.User{ID: 1} url := user.APActorKeyID() expected := "https://try.gitea.io/api/v1/activitypub/user-id/1#main-key" diff --git a/modules/git/blob.go b/modules/git/blob.go index 8c5c275146..3fda358938 100644 --- a/modules/git/blob.go +++ b/modules/git/blob.go @@ -12,6 +12,7 @@ import ( "forgejo.org/modules/log" "forgejo.org/modules/typesniffer" + "forgejo.org/modules/util" ) // Blob represents a Git object. @@ -24,25 +25,42 @@ type Blob struct { repo *Repository } -func (b *Blob) newReader() (*bufio.Reader, int64, func(), error) { +// 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) { wr, rd, cancel, err := b.repo.CatFileBatch(b.repo.Ctx) if err != nil { - return nil, 0, nil, err + return nil, err } _, err = wr.Write([]byte(b.ID.String() + "\n")) if err != nil { cancel() - return nil, 0, nil, err + return nil, err } _, _, size, err := ReadBatchLine(rd) if err != nil { cancel() - return nil, 0, nil, err + return nil, err } b.gotSize = true b.size = size - return rd, size, cancel, err + + if size < 4096 { + bs, err := io.ReadAll(io.LimitReader(rd, size)) + defer cancel() + if err != nil { + return nil, err + } + _, err = rd.Discard(1) + return io.NopCloser(bytes.NewReader(bs)), err + } + + return &blobReader{ + rd: rd, + n: size, + cancel: cancel, + }, nil } // Size returns the uncompressed size of the blob @@ -73,36 +91,10 @@ func (b *Blob) Size() int64 { return b.size } -// 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) { - rd, size, cancel, err := b.newReader() - if err != nil { - return nil, err - } - - if size < 4096 { - bs, err := io.ReadAll(io.LimitReader(rd, size)) - defer cancel() - if err != nil { - return nil, err - } - _, err = rd.Discard(1) - return io.NopCloser(bytes.NewReader(bs)), err - } - - return &blobReader{ - rd: rd, - n: size, - cancel: cancel, - }, nil -} - type blobReader struct { - rd *bufio.Reader - n int64 // number of bytes to read - additionalDiscard int64 // additional number of bytes to discard - cancel func() + rd *bufio.Reader + n int64 + cancel func() } func (b *blobReader) Read(p []byte) (n int, err error) { @@ -125,8 +117,7 @@ func (b *blobReader) Close() error { defer b.cancel() - // discard the unread bytes, the truncated bytes and the trailing newline - if err := DiscardFull(b.rd, b.n+b.additionalDiscard+1); err != nil { + if err := DiscardFull(b.rd, b.n+1); err != nil { return err } @@ -140,35 +131,17 @@ func (b *Blob) Name() string { return b.name } -// NewTruncatedReader return a blob-reader which silently truncates when the limit is reached (io.EOF will be returned) -func (b *Blob) NewTruncatedReader(limit int64) (rc io.ReadCloser, fullSize int64, err error) { - r, fullSize, cancel, err := b.newReader() - if err != nil { - return nil, fullSize, err - } - - limit = min(limit, fullSize) - return &blobReader{ - rd: r, - n: limit, - additionalDiscard: fullSize - limit, - cancel: cancel, - }, fullSize, nil -} - -// GetBlobContent Gets the truncated content of the blob as raw text +// GetBlobContent Gets the limited content of the blob as raw text func (b *Blob) GetBlobContent(limit int64) (string, error) { if limit <= 0 { return "", nil } - rc, fullSize, err := b.NewTruncatedReader(limit) + dataRc, err := b.DataAsync() if err != nil { return "", err } - defer rc.Close() - - buf := make([]byte, min(fullSize, limit)) - _, err = io.ReadFull(rc, buf) + defer dataRc.Close() + buf, err := util.ReadWithLimit(dataRc, int(limit)) return string(buf), err } diff --git a/modules/git/blob_test.go b/modules/git/blob_test.go index 54115013d3..810964b33d 100644 --- a/modules/git/blob_test.go +++ b/modules/git/blob_test.go @@ -35,106 +35,6 @@ func TestBlob_Data(t *testing.T) { assert.Equal(t, output, string(data)) } -func TestBlob(t *testing.T) { - bareRepo1Path := filepath.Join(testReposDir, "repo1_bare") - repo, err := openRepositoryWithDefaultContext(bareRepo1Path) - require.NoError(t, err) - - defer repo.Close() - - testBlob, err := repo.GetBlob("6c493ff740f9380390d5c9ddef4af18697ac9375") - require.NoError(t, err) - - t.Run("GetBlobContent", func(t *testing.T) { - r, err := testBlob.GetBlobContent(100) - require.NoError(t, err) - require.Equal(t, "file2\n", r) - - r, err = testBlob.GetBlobContent(-1) - require.NoError(t, err) - require.Empty(t, r) - - r, err = testBlob.GetBlobContent(4) - require.NoError(t, err) - require.Equal(t, "file", r) - - r, err = testBlob.GetBlobContent(6) - require.NoError(t, err) - require.Equal(t, "file2\n", r) - }) - - t.Run("NewTruncatedReader", func(t *testing.T) { - // read fewer than available - rc, size, err := testBlob.NewTruncatedReader(100) - require.NoError(t, err) - require.Equal(t, int64(6), size) - - buf := make([]byte, 1) - n, err := rc.Read(buf) - require.NoError(t, err) - require.Equal(t, 1, n) - require.Equal(t, "f", string(buf)) - n, err = rc.Read(buf) - require.NoError(t, err) - require.Equal(t, 1, n) - require.Equal(t, "i", string(buf)) - - require.NoError(t, rc.Close()) - - // read more than available - rc, size, err = testBlob.NewTruncatedReader(100) - require.NoError(t, err) - require.Equal(t, int64(6), size) - - buf = make([]byte, 100) - n, err = rc.Read(buf) - require.NoError(t, err) - require.Equal(t, 6, n) - require.Equal(t, "file2\n", string(buf[:n])) - - n, err = rc.Read(buf) - require.Error(t, err) - require.Equal(t, io.EOF, err) - require.Equal(t, 0, n) - - require.NoError(t, rc.Close()) - - // read more than truncated - rc, size, err = testBlob.NewTruncatedReader(4) - require.NoError(t, err) - require.Equal(t, int64(6), size) - - buf = make([]byte, 10) - n, err = rc.Read(buf) - require.NoError(t, err) - require.Equal(t, 4, n) - require.Equal(t, "file", string(buf[:n])) - - n, err = rc.Read(buf) - require.Error(t, err) - require.Equal(t, io.EOF, err) - require.Equal(t, 0, n) - - require.NoError(t, rc.Close()) - }) - - t.Run("NonExisting", func(t *testing.T) { - nonExistingBlob, err := repo.GetBlob("00003ff740f9380390d5c9ddef4af18690000000") - require.NoError(t, err) - - r, err := nonExistingBlob.GetBlobContent(100) - require.Error(t, err) - require.IsType(t, ErrNotExist{}, err) - require.Empty(t, r) - - rc, size, err := nonExistingBlob.NewTruncatedReader(100) - require.Error(t, err) - require.IsType(t, ErrNotExist{}, err) - require.Empty(t, rc) - require.Empty(t, size) - }) -} - func Benchmark_Blob_Data(b *testing.B) { bareRepo1Path := filepath.Join(testReposDir, "repo1_bare") repo, err := openRepositoryWithDefaultContext(bareRepo1Path) diff --git a/modules/markup/markdown/markdown.go b/modules/markup/markdown/markdown.go index e811d29994..717da464b9 100644 --- a/modules/markup/markdown/markdown.go +++ b/modules/markup/markdown/markdown.go @@ -267,13 +267,8 @@ func Render(ctx *markup.RenderContext, input io.Reader, output io.Writer) error // RenderString renders Markdown string to HTML with all specific handling stuff and return string func RenderString(ctx *markup.RenderContext, content string) (template.HTML, error) { - return RenderReader(ctx, strings.NewReader(content)) -} - -// RenderReader renders Markdown io.Reader to HTML with all specific handling stuff and return string -func RenderReader(ctx *markup.RenderContext, input io.Reader) (template.HTML, error) { var buf strings.Builder - if err := Render(ctx, input, &buf); err != nil { + if err := Render(ctx, strings.NewReader(content), &buf); err != nil { return "", err } return template.HTML(buf.String()), nil diff --git a/modules/public/mime_types.go b/modules/public/mime_types.go index 87ee2854ae..32bdf3bfa2 100644 --- a/modules/public/mime_types.go +++ b/modules/public/mime_types.go @@ -23,11 +23,6 @@ var wellKnownMimeTypesLower = map[string]string{ ".wasm": "application/wasm", ".webp": "image/webp", ".xml": "text/xml; charset=utf-8", - ".glb": "model/gltf-binary", - ".gltf": "model/gltf+json", - ".obj": "model/obj", - ".stl": "model/stl", - ".3mf": "model/3mf", // well, there are some types missing from the builtin list ".txt": "text/plain; charset=utf-8", diff --git a/modules/structs/action.go b/modules/structs/action.go index f47b228d75..2c42365c19 100644 --- a/modules/structs/action.go +++ b/modules/structs/action.go @@ -78,9 +78,3 @@ type ActionRun struct { // the url of this action run HTMLURL string `json:"html_url"` } - -// ListActionRunResponse return a list of ActionRun -type ListActionRunResponse struct { - Entries []*ActionRun `json:"workflow_runs"` - TotalCount int64 `json:"total_count"` -} diff --git a/modules/structs/repo_actions.go b/modules/structs/repo_actions.go index b13f344738..505367336c 100644 --- a/modules/structs/repo_actions.go +++ b/modules/structs/repo_actions.go @@ -32,3 +32,23 @@ type ActionTaskResponse struct { Entries []*ActionTask `json:"workflow_runs"` TotalCount int64 `json:"total_count"` } + +// ActionRun represents an ActionRun +type RepoActionRun struct { + ID int64 `json:"id"` + Name string `json:"name"` + RunNumber int64 `json:"run_number"` + Event string `json:"event"` + Status string `json:"status"` + HeadBranch string `json:"head_branch"` + HeadSHA string `json:"head_sha"` + WorkflowID string `json:"workflow_id"` + URL string `json:"url"` + TriggeringActor *User `json:"triggering_actor"` +} + +// ListActionRunResponse return a list of ActionRun +type ListRepoActionRunResponse struct { + Entries []*RepoActionRun `json:"workflow_runs"` + TotalCount int64 `json:"total_count"` +} diff --git a/modules/typesniffer/typesniffer.go b/modules/typesniffer/typesniffer.go index 262feb2b05..a8fc70e54c 100644 --- a/modules/typesniffer/typesniffer.go +++ b/modules/typesniffer/typesniffer.go @@ -24,16 +24,6 @@ const ( AvifMimeType = "image/avif" // ApplicationOctetStream MIME type of binary files. ApplicationOctetStream = "application/octet-stream" - // GLTFMimeType MIME type of GLTF files. - GLTFMimeType = "model/gltf+json" - // GLBMimeType MIME type of GLB files. - GLBMimeType = "model/gltf-binary" - // OBJMimeType MIME type of OBJ files. - OBJMimeType = "model/obj" - // STLMimeType MIME type of STL files. - STLMimeType = "model/stl" - // 3MFMimeType MIME type of 3MF files. - ThreeMFMimeType = "model/3mf" ) var ( @@ -77,36 +67,6 @@ func (ct SniffedType) IsAudio() bool { return strings.Contains(ct.contentType, "audio/") } -// Is3DModel detects if data is a 3D format -func (ct SniffedType) Is3DModel() bool { - return strings.Contains(ct.contentType, "model/") -} - -// IsGLTFFile detects if data is an SVG image format -func (ct SniffedType) IsGLTF() bool { - return strings.Contains(ct.contentType, GLTFMimeType) -} - -// IsGLBFile detects if data is an GLB image format -func (ct SniffedType) IsGLB() bool { - return strings.Contains(ct.contentType, GLBMimeType) -} - -// IsOBJFile detects if data is an OBJ image format -func (ct SniffedType) IsOBJ() bool { - return strings.Contains(ct.contentType, OBJMimeType) -} - -// IsSTLTextFile detects if data is an STL text format -func (ct SniffedType) IsSTL() bool { - return strings.Contains(ct.contentType, STLMimeType) -} - -// Is3MFFile detects if data is an 3MF image format -func (ct SniffedType) Is3MF() bool { - return strings.Contains(ct.contentType, ThreeMFMimeType) -} - // IsRepresentableAsText returns true if file content can be represented as // plain text or is empty. func (ct SniffedType) IsRepresentableAsText() bool { @@ -115,7 +75,7 @@ func (ct SniffedType) IsRepresentableAsText() bool { // IsBrowsableBinaryType returns whether a non-text type can be displayed in a browser func (ct SniffedType) IsBrowsableBinaryType() bool { - return ct.IsImage() || ct.IsSvgImage() || ct.IsPDF() || ct.IsVideo() || ct.IsAudio() || ct.Is3DModel() + return ct.IsImage() || ct.IsSvgImage() || ct.IsPDF() || ct.IsVideo() || ct.IsAudio() } // GetMimeType returns the mime type @@ -175,13 +135,6 @@ func DetectContentType(data []byte) SniffedType { ct = "audio/ogg" // for most cases, it is used as an audio container } } - - // GLTF is unsupported by http.DetectContentType - // hexdump -n 4 -C glTF.glb - if bytes.HasPrefix(data, []byte("glTF")) { - ct = GLBMimeType - } - return SniffedType{ct} } diff --git a/modules/typesniffer/typesniffer_test.go b/modules/typesniffer/typesniffer_test.go index 176d3658bb..8d80b4ddb4 100644 --- a/modules/typesniffer/typesniffer_test.go +++ b/modules/typesniffer/typesniffer_test.go @@ -117,14 +117,6 @@ func TestIsAudio(t *testing.T) { assert.True(t, DetectContentType([]byte("ID3Toy\n====\t* hi 🌞, ..."+"🌛"[0:2])).IsText()) // test ID3 tag with incomplete UTF8 char } -func TestIsGLB(t *testing.T) { - glb, _ := hex.DecodeString("676c5446") - assert.True(t, DetectContentType(glb).IsGLB()) - assert.True(t, DetectContentType(glb).Is3DModel()) - assert.False(t, DetectContentType([]byte("plain text")).IsGLB()) - assert.False(t, DetectContentType([]byte("plain text")).Is3DModel()) -} - func TestDetectContentTypeFromReader(t *testing.T) { mp3, _ := base64.StdEncoding.DecodeString("SUQzBAAAAAABAFRYWFgAAAASAAADbWFqb3JfYnJhbmQAbXA0MgBUWFhYAAAAEQAAA21pbm9yX3Zl") st, err := DetectContentTypeFromReader(bytes.NewReader(mp3)) @@ -153,15 +145,3 @@ func TestDetectContentTypeAvif(t *testing.T) { assert.True(t, st.IsImage()) } - -func TestDetectContentTypeModelGLB(t *testing.T) { - glb, err := hex.DecodeString("676c5446") - require.NoError(t, err) - - st, err := DetectContentTypeFromReader(bytes.NewReader(glb)) - require.NoError(t, err) - - // print st for debugging - assert.Equal(t, "model/gltf-binary", st.GetMimeType()) - assert.True(t, st.IsGLB()) -} diff --git a/modules/util/io.go b/modules/util/io.go index 4c99004c0c..1559b019a0 100644 --- a/modules/util/io.go +++ b/modules/util/io.go @@ -4,6 +4,7 @@ package util import ( + "bytes" "errors" "io" ) @@ -19,6 +20,42 @@ func ReadAtMost(r io.Reader, buf []byte) (n int, err error) { return n, err } +// ReadWithLimit reads at most "limit" bytes from r into buf. +// If EOF or ErrUnexpectedEOF occurs while reading, err will be nil. +func ReadWithLimit(r io.Reader, n int) (buf []byte, err error) { + return readWithLimit(r, 1024, n) +} + +func readWithLimit(r io.Reader, batch, limit int) ([]byte, error) { + if limit <= batch { + buf := make([]byte, limit) + n, err := ReadAtMost(r, buf) + if err != nil { + return nil, err + } + return buf[:n], nil + } + res := bytes.NewBuffer(make([]byte, 0, batch)) + bufFix := make([]byte, batch) + eof := false + for res.Len() < limit && !eof { + bufTmp := bufFix + if res.Len()+batch > limit { + bufTmp = bufFix[:limit-res.Len()] + } + n, err := io.ReadFull(r, bufTmp) + if err == io.EOF || err == io.ErrUnexpectedEOF { + eof = true + } else if err != nil { + return nil, err + } + if _, err = res.Write(bufTmp[:n]); err != nil { + return nil, err + } + } + return res.Bytes(), nil +} + // ErrNotEmpty is an error reported when there is a non-empty reader var ErrNotEmpty = errors.New("not-empty") diff --git a/modules/util/io_test.go b/modules/util/io_test.go new file mode 100644 index 0000000000..870e713646 --- /dev/null +++ b/modules/util/io_test.go @@ -0,0 +1,67 @@ +// Copyright 2023 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package util + +import ( + "bytes" + "errors" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +type readerWithError struct { + buf *bytes.Buffer +} + +func (r *readerWithError) Read(p []byte) (n int, err error) { + if r.buf.Len() < 2 { + return 0, errors.New("test error") + } + return r.buf.Read(p) +} + +func TestReadWithLimit(t *testing.T) { + bs := []byte("0123456789abcdef") + + // normal test + buf, err := readWithLimit(bytes.NewBuffer(bs), 5, 2) + require.NoError(t, err) + assert.Equal(t, []byte("01"), buf) + + buf, err = readWithLimit(bytes.NewBuffer(bs), 5, 5) + require.NoError(t, err) + assert.Equal(t, []byte("01234"), buf) + + buf, err = readWithLimit(bytes.NewBuffer(bs), 5, 6) + require.NoError(t, err) + assert.Equal(t, []byte("012345"), buf) + + buf, err = readWithLimit(bytes.NewBuffer(bs), 5, len(bs)) + require.NoError(t, err) + assert.Equal(t, []byte("0123456789abcdef"), buf) + + buf, err = readWithLimit(bytes.NewBuffer(bs), 5, 100) + require.NoError(t, err) + assert.Equal(t, []byte("0123456789abcdef"), buf) + + // test with error + buf, err = readWithLimit(&readerWithError{bytes.NewBuffer(bs)}, 5, 10) + require.NoError(t, err) + assert.Equal(t, []byte("0123456789"), buf) + + buf, err = readWithLimit(&readerWithError{bytes.NewBuffer(bs)}, 5, 100) + require.ErrorContains(t, err, "test error") + assert.Empty(t, buf) + + // test public function + buf, err = ReadWithLimit(bytes.NewBuffer(bs), 2) + require.NoError(t, err) + assert.Equal(t, []byte("01"), buf) + + buf, err = ReadWithLimit(bytes.NewBuffer(bs), 9999999) + require.NoError(t, err) + assert.Equal(t, []byte("0123456789abcdef"), buf) +} diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini index 0bb128f8b3..101591d5a9 100644 --- a/options/locale/locale_en-US.ini +++ b/options/locale/locale_en-US.ini @@ -768,8 +768,8 @@ update_profile_success = Your profile has been updated. change_username = Your username has been changed. change_username_prompt = Note: Changing your username also changes your account URL. change_username_redirect_prompt = The old username will redirect until someone claims it. -change_username_redirect_prompt.with_cooldown.one = The old username will be available to everyone after a cooldown period of %[1]d day. You can still reclaim the old username during the cooldown period. -change_username_redirect_prompt.with_cooldown.few = The old username will be available to everyone after a cooldown period of %[1]d days. You can still reclaim the old username during the cooldown period. +change_username_redirect_prompt.with_cooldown.one = The old username will be available to everyone after a cooldown period of %[1]d day, you can still reclaim the old username during the cooldown period. +change_username_redirect_prompt.with_cooldown.few = The old username will be available to everyone after a cooldown period of %[1]d days, you can still reclaim the old username during the cooldown period. continue = Continue cancel = Cancel language = Language @@ -1694,13 +1694,15 @@ issues.close_comment_issue = Close with comment issues.reopen_issue = Reopen issues.reopen_comment_issue = Reopen with comment issues.create_comment = Comment -issues.closed_at = `closed this issue %s` -issues.reopened_at = `reopened this issue %s` -issues.commit_ref_at = `referenced this issue from a commit %s` -issues.ref_issue_from = `referenced this issue %[3]s %[1]s` -issues.ref_pull_from = `referenced this pull request %[3]s %[1]s` -issues.ref_closing_from = `referenced this issue from a pull request %[3]s that will close it, %[1]s` -issues.ref_reopening_from = `referenced this issue from a pull request %[3]s that will reopen it, %[1]s` +issues.closed_at = `closed this issue %[2]s` +issues.reopened_at = `reopened this issue %[2]s` +issues.commit_ref_at = `referenced this issue from a commit %[2]s` +issues.ref_issue_from = `referenced this issue %[4]s %[2]s` +issues.ref_pull_from = `referenced this pull request %[4]s %[2]s` +issues.ref_closing_from = `referenced this issue from a pull request %[4]s that will close it, %[2]s` +issues.ref_reopening_from = `referenced this issue from a pull request %[4]s that will reopen it, %[2]s` +issues.ref_closed_from = `closed this issue %[4]s %[2]s` +issues.ref_reopened_from = `reopened this issue %[4]s %[2]s` issues.ref_from = `from %[1]s` issues.author = Author issues.author.tooltip.issue = This user is the author of this issue. @@ -2012,9 +2014,9 @@ pulls.update_branch_success = Branch update was successful pulls.update_not_allowed = You are not allowed to update branch pulls.outdated_with_base_branch = This branch is out-of-date with the base branch pulls.close = Close pull request -pulls.closed_at = `closed this pull request %s` -pulls.reopened_at = `reopened this pull request %s` -pulls.commit_ref_at = `referenced this pull request from a commit %s` +pulls.closed_at = `closed this pull request %[2]s` +pulls.reopened_at = `reopened this pull request %[2]s` +pulls.commit_ref_at = `referenced this pull request from a commit %[2]s` pulls.cmd_instruction_hint = View command line instructions pulls.cmd_instruction_checkout_title = Checkout pulls.cmd_instruction_checkout_desc = From your project repository, check out a new branch and test the changes. @@ -2931,8 +2933,8 @@ settings.update_settings = Update settings settings.update_setting_success = Organization settings have been updated. settings.change_orgname_prompt = Note: Changing the organization name will also change your organization's URL and free the old name. settings.change_orgname_redirect_prompt = The old name will redirect until it is claimed. -settings.change_orgname_redirect_prompt.with_cooldown.one = The old organization name will be available to everyone after a cooldown period of %[1]d day. You can still reclaim the old name during the cooldown period. -settings.change_orgname_redirect_prompt.with_cooldown.few = The old organization name will be available to everyone after a cooldown period of %[1]d days. You can still reclaim the old name during the cooldown period. +settings.change_orgname_redirect_prompt.with_cooldown.one = The old organization name will be available to everyone after a cooldown period of %[1]d day, you can still reclaim the old name during the cooldown period. +settings.change_orgname_redirect_prompt.with_cooldown.few = The old organization name will be available to everyone after a cooldown period of %[1]d days, you can still reclaim the old name during the cooldown period. settings.update_avatar_success = The organization's avatar has been updated. settings.delete = Delete organization settings.delete_account = Delete this organization diff --git a/package-lock.json b/package-lock.json index 9de06a8055..179c0e496c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,7 +12,6 @@ "@github/markdown-toolbar-element": "2.2.3", "@github/quote-selection": "2.1.0", "@github/text-expander-element": "2.8.0", - "@google/model-viewer": "4.1.0", "@mcaptcha/vanilla-glue": "0.1.0-alpha-3", "@primer/octicons": "19.14.0", "ansi_up": "6.0.5", @@ -63,7 +62,7 @@ "devDependencies": { "@axe-core/playwright": "4.10.2", "@eslint-community/eslint-plugin-eslint-comments": "4.5.0", - "@playwright/test": "1.52.0", + "@playwright/test": "1.53.0", "@stoplight/spectral-cli": "6.15.0", "@stylistic/eslint-plugin": "4.4.1", "@stylistic/stylelint-plugin": "3.1.2", @@ -1223,22 +1222,6 @@ "dom-input-range": "^1.2.0" } }, - "node_modules/@google/model-viewer": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/@google/model-viewer/-/model-viewer-4.1.0.tgz", - "integrity": "sha512-7WB/jS6wfBfRl/tWhsUUvDMKFE1KlKME97coDLlZQfvJD0nCwjhES1lJ+k7wnmf7T3zMvCfn9mIjM/mgZapuig==", - "license": "Apache-2.0", - "dependencies": { - "@monogrid/gainmap-js": "^3.1.0", - "lit": "^3.2.1" - }, - "engines": { - "node": ">=6.0.0" - }, - "peerDependencies": { - "three": "^0.172.0" - } - }, "node_modules/@humanfs/core": { "version": "0.19.1", "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", @@ -2021,21 +2004,6 @@ "integrity": "sha512-M5UknZPHRu3DEDWoipU6sE8PdkZ6Z/S+v4dD+Ke8IaNlpdSQah50lz1KtcFBa2vsdOnwbbnxJwVM4wty6udA5w==", "license": "MIT" }, - "node_modules/@lit-labs/ssr-dom-shim": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/@lit-labs/ssr-dom-shim/-/ssr-dom-shim-1.3.0.tgz", - "integrity": "sha512-nQIWonJ6eFAvUUrSlwyHDm/aE8PBDu5kRpL0vHMg6K8fK3Diq1xdPjTnsJSwxABhaZ+5eBi1btQB5ShUTKo4nQ==", - "license": "BSD-3-Clause" - }, - "node_modules/@lit/reactive-element": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/@lit/reactive-element/-/reactive-element-2.1.0.tgz", - "integrity": "sha512-L2qyoZSQClcBmq0qajBVbhYEcG6iK0XfLn66ifLe/RfC0/ihpc+pl0Wdn8bJ8o+hj38cG0fGXRgSS20MuXn7qA==", - "license": "BSD-3-Clause", - "dependencies": { - "@lit-labs/ssr-dom-shim": "^1.2.0" - } - }, "node_modules/@mcaptcha/core-glue": { "version": "0.1.0-alpha-5", "resolved": "https://registry.npmjs.org/@mcaptcha/core-glue/-/core-glue-0.1.0-alpha-5.tgz", @@ -2096,18 +2064,6 @@ "langium": "3.3.1" } }, - "node_modules/@monogrid/gainmap-js": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/@monogrid/gainmap-js/-/gainmap-js-3.1.0.tgz", - "integrity": "sha512-Obb0/gEd/HReTlg8ttaYk+0m62gQJmCblMOjHSMHRrBP2zdfKMHLCRbh/6ex9fSUJMKdjjIEiohwkbGD3wj2Nw==", - "license": "MIT", - "dependencies": { - "promise-worker-transferable": "^1.0.4" - }, - "peerDependencies": { - "three": ">= 0.159.0" - } - }, "node_modules/@napi-rs/wasm-runtime": { "version": "0.2.11", "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-0.2.11.tgz", @@ -2187,13 +2143,13 @@ } }, "node_modules/@playwright/test": { - "version": "1.52.0", - "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.52.0.tgz", - "integrity": "sha512-uh6W7sb55hl7D6vsAeA+V2p5JnlAqzhqFyF0VcJkKZXkgnFcVG9PziERRHQfPLfNGx1C292a4JqbWzhR8L4R1g==", + "version": "1.53.0", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.53.0.tgz", + "integrity": "sha512-15hjKreZDcp7t6TL/7jkAo6Df5STZN09jGiv5dbP9A6vMVncXRqE7/B2SncsyOwrkZRBH2i6/TPOL8BVmm3c7w==", "dev": true, "license": "Apache-2.0", "dependencies": { - "playwright": "1.52.0" + "playwright": "1.53.0" }, "bin": { "playwright": "cli.js" @@ -3537,7 +3493,8 @@ "version": "2.0.7", "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz", "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==", - "license": "MIT" + "license": "MIT", + "optional": true }, "node_modules/@types/unist": { "version": "2.0.11", @@ -8811,12 +8768,6 @@ "node": ">= 4" } }, - "node_modules/immediate": { - "version": "3.0.6", - "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz", - "integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==", - "license": "MIT" - }, "node_modules/immer": { "version": "9.0.21", "resolved": "https://registry.npmjs.org/immer/-/immer-9.0.21.tgz", @@ -9356,12 +9307,6 @@ "dev": true, "license": "MIT" }, - "node_modules/is-promise": { - "version": "2.2.2", - "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-2.2.2.tgz", - "integrity": "sha512-+lP4/6lKUBfQjZ2pdxThZvLUAafmZb8OAxFb8XXtiQmS35INgr85hdOGoEs124ez1FCnZJt6jau/T+alh58QFQ==", - "license": "MIT" - }, "node_modules/is-proto-prop": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/is-proto-prop/-/is-proto-prop-3.0.1.tgz", @@ -10078,15 +10023,6 @@ "npm": ">=8" } }, - "node_modules/lie": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz", - "integrity": "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==", - "license": "MIT", - "dependencies": { - "immediate": "~3.0.5" - } - }, "node_modules/lilconfig": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz", @@ -10115,37 +10051,6 @@ "uc.micro": "^2.0.0" } }, - "node_modules/lit": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/lit/-/lit-3.3.0.tgz", - "integrity": "sha512-DGVsqsOIHBww2DqnuZzW7QsuCdahp50ojuDaBPC7jUDRpYoH0z7kHBBYZewRzer75FwtrkmkKk7iOAwSaWdBmw==", - "license": "BSD-3-Clause", - "dependencies": { - "@lit/reactive-element": "^2.1.0", - "lit-element": "^4.2.0", - "lit-html": "^3.3.0" - } - }, - "node_modules/lit-element": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/lit-element/-/lit-element-4.2.0.tgz", - "integrity": "sha512-MGrXJVAI5x+Bfth/pU9Kst1iWID6GHDLEzFEnyULB/sFiRLgkd8NPK/PeeXxktA3T6EIIaq8U3KcbTU5XFcP2Q==", - "license": "BSD-3-Clause", - "dependencies": { - "@lit-labs/ssr-dom-shim": "^1.2.0", - "@lit/reactive-element": "^2.1.0", - "lit-html": "^3.3.0" - } - }, - "node_modules/lit-html": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/lit-html/-/lit-html-3.3.0.tgz", - "integrity": "sha512-RHoswrFAxY2d8Cf2mm4OZ1DgzCoBKUKSPvA1fhtSELxUERq2aQQ2h05pO9j81gS1o7RIRJ+CePLogfyahwmynw==", - "license": "BSD-3-Clause", - "dependencies": { - "@types/trusted-types": "^2.0.2" - } - }, "node_modules/loader-runner": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.3.0.tgz", @@ -11954,13 +11859,13 @@ } }, "node_modules/playwright": { - "version": "1.52.0", - "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.52.0.tgz", - "integrity": "sha512-JAwMNMBlxJ2oD1kce4KPtMkDeKGHQstdpFPcPH3maElAXon/QZeTvtsfXmTMRyO9TslfoYOXkSsvao2nE1ilTw==", + "version": "1.53.0", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.53.0.tgz", + "integrity": "sha512-ghGNnIEYZC4E+YtclRn4/p6oYbdPiASELBIYkBXfaTVKreQUYbMUYQDwS12a8F0/HtIjr/CkGjtwABeFPGcS4Q==", "dev": true, "license": "Apache-2.0", "dependencies": { - "playwright-core": "1.52.0" + "playwright-core": "1.53.0" }, "bin": { "playwright": "cli.js" @@ -11973,9 +11878,9 @@ } }, "node_modules/playwright-core": { - "version": "1.52.0", - "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.52.0.tgz", - "integrity": "sha512-l2osTgLXSMeuLZOML9qYODUQoPPnUsKsb5/P6LJ2e6uPKXUdPK5WYhN4z03G+YNbWmGDY4YENauNu4ZKczreHg==", + "version": "1.53.0", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.53.0.tgz", + "integrity": "sha512-mGLg8m0pm4+mmtB7M89Xw/GSqoNC+twivl8ITteqvAndachozYe2ZA7srU6uleV1vEdAHYqjq+SV8SNxRRFYBw==", "dev": true, "license": "Apache-2.0", "bin": { @@ -12458,16 +12363,6 @@ "dev": true, "license": "Unlicense" }, - "node_modules/promise-worker-transferable": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/promise-worker-transferable/-/promise-worker-transferable-1.0.4.tgz", - "integrity": "sha512-bN+0ehEnrXfxV2ZQvU2PetO0n4gqBD4ulq3MI1WOPLgr7/Mg9yRQkX5+0v1vagr74ZTsl7XtzlaYDo2EuCeYJw==", - "license": "Apache-2.0", - "dependencies": { - "is-promise": "^2.1.0", - "lie": "^3.0.2" - } - }, "node_modules/proto-list": { "version": "1.2.4", "resolved": "https://registry.npmjs.org/proto-list/-/proto-list-1.2.4.tgz", @@ -14530,13 +14425,6 @@ "node": ">=0.8" } }, - "node_modules/three": { - "version": "0.172.0", - "resolved": "https://registry.npmjs.org/three/-/three-0.172.0.tgz", - "integrity": "sha512-6HMgMlzU97MsV7D/tY8Va38b83kz8YJX+BefKjspMNAv0Vx6dxMogHOrnRl/sbMIs3BPUKijPqDqJ/+UwJbIow==", - "license": "MIT", - "peer": true - }, "node_modules/throttle-debounce": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/throttle-debounce/-/throttle-debounce-5.0.0.tgz", diff --git a/package.json b/package.json index f7df1b3f38..409edecd95 100644 --- a/package.json +++ b/package.json @@ -11,7 +11,6 @@ "@github/markdown-toolbar-element": "2.2.3", "@github/quote-selection": "2.1.0", "@github/text-expander-element": "2.8.0", - "@google/model-viewer": "4.1.0", "@mcaptcha/vanilla-glue": "0.1.0-alpha-3", "@primer/octicons": "19.14.0", "ansi_up": "6.0.5", @@ -62,7 +61,7 @@ "devDependencies": { "@axe-core/playwright": "4.10.2", "@eslint-community/eslint-plugin-eslint-comments": "4.5.0", - "@playwright/test": "1.52.0", + "@playwright/test": "1.53.0", "@stoplight/spectral-cli": "6.15.0", "@stylistic/eslint-plugin": "4.4.1", "@stylistic/stylelint-plugin": "3.1.2", @@ -79,8 +78,8 @@ "eslint-plugin-playwright": "2.2.0", "eslint-plugin-regexp": "2.9.0", "eslint-plugin-sonarjs": "3.0.2", - "eslint-plugin-toml": "0.12.0", "eslint-plugin-unicorn": "59.0.1", + "eslint-plugin-toml": "0.12.0", "eslint-plugin-vitest-globals": "1.5.0", "eslint-plugin-vue": "10.2.0", "eslint-plugin-vue-scoped-css": "2.10.0", diff --git a/routers/api/v1/repo/action.go b/routers/api/v1/repo/action.go index dbc4933de6..03089a18d3 100644 --- a/routers/api/v1/repo/action.go +++ b/routers/api/v1/repo/action.go @@ -748,7 +748,7 @@ func ListActionRuns(ctx *context.APIContext) { // type: string // responses: // "200": - // "$ref": "#/responses/ActionRunList" + // "$ref": "#/responses/RepoActionRunList" // "400": // "$ref": "#/responses/error" // "403": @@ -779,16 +779,16 @@ func ListActionRuns(ctx *context.APIContext) { return } - res := new(api.ListActionRunResponse) + res := new(api.ListRepoActionRunResponse) res.TotalCount = total - res.Entries = make([]*api.ActionRun, len(runs)) + res.Entries = make([]*api.RepoActionRun, len(runs)) for i, r := range runs { - if err := r.LoadAttributes(ctx); err != nil { - ctx.Error(http.StatusInternalServerError, "LoadAttributes", err) + cr, err := convert.ToRepoActionRun(ctx, r) + if err != nil { + ctx.Error(http.StatusInternalServerError, "ToActionRun", err) return } - cr := convert.ToActionRun(ctx, r, ctx.Doer) res.Entries[i] = cr } @@ -821,7 +821,7 @@ func GetActionRun(ctx *context.APIContext) { // required: true // responses: // "200": - // "$ref": "#/responses/ActionRun" + // "$ref": "#/responses/RepoActionRun" // "400": // "$ref": "#/responses/error" // "403": @@ -839,17 +839,16 @@ func GetActionRun(ctx *context.APIContext) { return } - // Action runs lives in its own table, therefore we check that the - // run with the requested ID is owned by the repository if ctx.Repo.Repository.ID != run.RepoID { ctx.Error(http.StatusNotFound, "GetRunById", util.ErrNotExist) return } - if err := run.LoadAttributes(ctx); err != nil { - ctx.Error(http.StatusInternalServerError, "LoadAttributes", err) + res, err := convert.ToRepoActionRun(ctx, run) + if err != nil { + ctx.Error(http.StatusInternalServerError, "ToRepoActionRun", err) return } - ctx.JSON(http.StatusOK, convert.ToActionRun(ctx, run, ctx.Doer)) + ctx.JSON(http.StatusOK, res) } diff --git a/routers/api/v1/swagger/repo.go b/routers/api/v1/swagger/repo.go index cd4832e15f..bde0efea4e 100644 --- a/routers/api/v1/swagger/repo.go +++ b/routers/api/v1/swagger/repo.go @@ -463,16 +463,16 @@ type swaggerSyncForkInfo struct { Body []api.SyncForkInfo `json:"body"` } -// ActionRunList -// swagger:response ActionRunList -type swaggerActionRunList struct { +// RepoActionRunList +// swagger:response RepoActionRunList +type swaggerRepoActionRunList struct { // in:body - Body api.ListActionRunResponse `json:"body"` + Body api.ListRepoActionRunResponse `json:"body"` } -// ActionRun -// swagger:response ActionRun -type swaggerActionRun struct { +// RepoActionRun +// swagger:response RepoActionRun +type swaggerRepoActionRun struct { // in:body - Body api.ActionRun `json:"body"` + Body api.RepoActionRun `json:"body"` } diff --git a/routers/web/org/home.go b/routers/web/org/home.go index 8f14f8899c..a3823565ed 100644 --- a/routers/web/org/home.go +++ b/routers/web/org/home.go @@ -175,12 +175,10 @@ func prepareOrgProfileReadme(ctx *context.Context, profileGitRepo *git.Repositor return } - if rc, _, err := profileReadme.NewTruncatedReader(setting.UI.MaxDisplayFileSize); err != nil { - log.Error("failed to NewTruncatedReader: %v", err) + if bytes, err := profileReadme.GetBlobContent(setting.UI.MaxDisplayFileSize); err != nil { + log.Error("failed to GetBlobContent: %v", err) } else { - defer rc.Close() - - if profileContent, err := markdown.RenderReader(&markup.RenderContext{ + if profileContent, err := markdown.RenderString(&markup.RenderContext{ Ctx: ctx, GitRepo: profileGitRepo, Links: markup.Links{ @@ -190,7 +188,7 @@ func prepareOrgProfileReadme(ctx *context.Context, profileGitRepo *git.Repositor BranchPath: path.Join("branch", util.PathEscapeSegments(profileDbRepo.DefaultBranch)), }, Metas: map[string]string{"mode": "document"}, - }, rc); err != nil { + }, bytes); err != nil { log.Error("failed to RenderString: %v", err) } else { ctx.Data["ProfileReadme"] = profileContent diff --git a/routers/web/repo/issue.go b/routers/web/repo/issue.go index 3f17e30cec..d8f3bd8d9f 100644 --- a/routers/web/repo/issue.go +++ b/routers/web/repo/issue.go @@ -1308,7 +1308,7 @@ func roleDescriptor(ctx stdCtx.Context, repo *repo_model.Repository, poster *use } // Special user that can't have associated contributions and permissions in the repo. - if poster.IsSystem() || poster.IsAPServerActor() { + if poster.IsGhost() || poster.IsActions() || poster.IsAPServerActor() { return roleDescriptor, nil } diff --git a/routers/web/repo/setting/lfs.go b/routers/web/repo/setting/lfs.go index b9cb86bd08..2e9c34e8a7 100644 --- a/routers/web/repo/setting/lfs.go +++ b/routers/web/repo/setting/lfs.go @@ -342,20 +342,6 @@ func LFSFileGet(ctx *context.Context) { ctx.Data["IsVideoFile"] = true case st.IsAudio(): ctx.Data["IsAudioFile"] = true - case st.Is3DModel(): - ctx.Data["Is3DModelFile"] = true - switch { - case st.IsGLB(): - ctx.Data["IsGLBFile"] = true - case st.IsSTL(): - ctx.Data["IsSTLFile"] = true - case st.IsGLTF(): - ctx.Data["IsGLTFFile"] = true - case st.IsOBJ(): - ctx.Data["IsOBJFile"] = true - case st.Is3MF(): - ctx.Data["Is3MFFile"] = true - } case st.IsImage() && (setting.UI.SVG.Enabled || !st.IsSvgImage()): ctx.Data["IsImageFile"] = true } diff --git a/routers/web/repo/setting/setting.go b/routers/web/repo/setting/setting.go index 6f35e19880..b65d1fbc92 100644 --- a/routers/web/repo/setting/setting.go +++ b/routers/web/repo/setting/setting.go @@ -153,9 +153,11 @@ func UnitsPost(ctx *context.Context) { }) deleteUnitTypes = append(deleteUnitTypes, unit_model.TypeWiki) } else if form.EnableWiki && !form.EnableExternalWiki && !unit_model.TypeWiki.UnitGlobalDisabled() { - wikiPermissions := repo_model.UnitAccessModeUnset + var wikiPermissions repo_model.UnitAccessMode if form.GloballyWriteableWiki { wikiPermissions = repo_model.UnitAccessModeWrite + } else { + wikiPermissions = repo_model.UnitAccessModeRead } units = append(units, repo_model.RepoUnit{ RepoID: repo.ID, diff --git a/routers/web/repo/view.go b/routers/web/repo/view.go index bb3e1388a8..c7cc715fc1 100644 --- a/routers/web/repo/view.go +++ b/routers/web/repo/view.go @@ -439,8 +439,8 @@ func renderFile(ctx *context.Context, entry *git.TreeEntry) { ctx.Data["FileError"] = ctx.Locale.Tr("actions.runs.invalid_workflow_helper", workFlowErr.Error()) } } else if slices.Contains([]string{"CODEOWNERS", "docs/CODEOWNERS", ".gitea/CODEOWNERS"}, ctx.Repo.TreePath) { - if rc, size, err := blob.NewTruncatedReader(setting.UI.MaxDisplayFileSize); err == nil { - _, warnings := issue_model.GetCodeOwnersFromReader(ctx, rc, size > setting.UI.MaxDisplayFileSize) + if data, err := blob.GetBlobContent(setting.UI.MaxDisplayFileSize); err == nil { + _, warnings := issue_model.GetCodeOwnersFromContent(ctx, data) if len(warnings) > 0 { ctx.Data["FileWarning"] = strings.Join(warnings, "\n") } @@ -624,20 +624,6 @@ func renderFile(ctx *context.Context, entry *git.TreeEntry) { ctx.Data["IsVideoFile"] = true case fInfo.st.IsAudio(): ctx.Data["IsAudioFile"] = true - case fInfo.st.Is3DModel(): - ctx.Data["Is3DModelFile"] = true - switch { - case fInfo.st.IsGLB(): - ctx.Data["IsGLBFile"] = true - case fInfo.st.IsSTL(): - ctx.Data["IsSTLFile"] = true - case fInfo.st.IsGLTF(): - ctx.Data["IsGLTFFile"] = true - case fInfo.st.IsOBJ(): - ctx.Data["IsOBJFile"] = true - case fInfo.st.Is3MF(): - ctx.Data["Is3MFFile"] = true - } case fInfo.st.IsImage() && (setting.UI.SVG.Enabled || !fInfo.st.IsSvgImage()): ctx.Data["IsImageFile"] = true ctx.Data["CanCopyContent"] = true diff --git a/routers/web/user/profile.go b/routers/web/user/profile.go index 78dd6c5e7c..58f6d4a5f2 100644 --- a/routers/web/user/profile.go +++ b/routers/web/user/profile.go @@ -264,12 +264,10 @@ func prepareUserProfileTabData(ctx *context.Context, showPrivate bool, profileDb total = int(count) case "overview": - if rc, _, err := profileReadme.NewTruncatedReader(setting.UI.MaxDisplayFileSize); err != nil { - log.Error("failed to NewTruncatedReader: %v", err) + if bytes, err := profileReadme.GetBlobContent(setting.UI.MaxDisplayFileSize); err != nil { + log.Error("failed to GetBlobContent: %v", err) } else { - defer rc.Close() - - if profileContent, err := markdown.RenderReader(&markup.RenderContext{ + if profileContent, err := markdown.RenderString(&markup.RenderContext{ Ctx: ctx, GitRepo: profileGitRepo, Links: markup.Links{ @@ -282,7 +280,7 @@ func prepareUserProfileTabData(ctx *context.Context, showPrivate bool, profileDb BranchPath: path.Join("branch", util.PathEscapeSegments(profileDbRepo.DefaultBranch)), }, Metas: map[string]string{"mode": "document"}, - }, rc); err != nil { + }, bytes); err != nil { log.Error("failed to RenderString: %v", err) } else { ctx.Data["ProfileReadme"] = profileContent diff --git a/routers/web/user/setting/profile.go b/routers/web/user/setting/profile.go index 400ee71f08..0ddb1b21f5 100644 --- a/routers/web/user/setting/profile.go +++ b/routers/web/user/setting/profile.go @@ -66,9 +66,6 @@ func ProfilePost(ctx *context.Context) { ctx.Data["DisableGravatar"] = setting.Config().Picture.DisableGravatar.Value(ctx) ctx.Data["CooldownPeriod"] = setting.Service.UsernameCooldownPeriod ctx.Data["CommonPronouns"] = commonPronouns - ctx.Data["MaxAvatarFileSize"] = setting.Avatar.MaxFileSize - ctx.Data["MaxAvatarWidth"] = setting.Avatar.MaxWidth - ctx.Data["MaxAvatarHeight"] = setting.Avatar.MaxHeight if ctx.HasError() { ctx.HTML(http.StatusOK, tplSettingsProfile) diff --git a/services/actions/notifier_helper.go b/services/actions/notifier_helper.go index e240c996b5..9654186fbb 100644 --- a/services/actions/notifier_helper.go +++ b/services/actions/notifier_helper.go @@ -345,14 +345,6 @@ func handleWorkflows( Status: actions_model.StatusWaiting, } - if workflow, err := model.ReadWorkflow(bytes.NewReader(dwf.Content)); err == nil { - notifications, err := workflow.Notifications() - if err != nil { - log.Error("Notifications: %w", err) - } - run.NotifyEmail = notifications - } - need, err := ifNeedApproval(ctx, run, input.Repo, input.Doer) if err != nil { log.Error("check if need approval for repo %d with user %d: %v", input.Repo.ID, input.Doer.ID, err) diff --git a/services/actions/schedule_tasks.go b/services/actions/schedule_tasks.go index cf8b29ead7..3ec0807d5f 100644 --- a/services/actions/schedule_tasks.go +++ b/services/actions/schedule_tasks.go @@ -4,7 +4,6 @@ package actions import ( - "bytes" "context" "errors" "fmt" @@ -19,7 +18,6 @@ import ( webhook_module "forgejo.org/modules/webhook" "github.com/nektos/act/pkg/jobparser" - act_model "github.com/nektos/act/pkg/model" "xorm.io/builder" ) @@ -142,16 +140,6 @@ func CreateScheduleTask(ctx context.Context, cron *actions_model.ActionSchedule) return err } - workflow, err := act_model.ReadWorkflow(bytes.NewReader(cron.Content)) - if err != nil { - return err - } - notifications, err := workflow.Notifications() - if err != nil { - return err - } - run.NotifyEmail = notifications - // Parse the workflow specification from the cron schedule workflows, err := jobparser.Parse(cron.Content, jobparser.WithVars(vars)) if err != nil { diff --git a/services/actions/schedule_tasks_test.go b/services/actions/schedule_tasks_test.go deleted file mode 100644 index 7073985252..0000000000 --- a/services/actions/schedule_tasks_test.go +++ /dev/null @@ -1,121 +0,0 @@ -// Copyright 2025 The Forgejo Authors. All rights reserved. -// SPDX-License-Identifier: GPL-3.0-or-later - -package actions - -import ( - "testing" - - actions_model "forgejo.org/models/actions" - repo_model "forgejo.org/models/repo" - "forgejo.org/models/unittest" - webhook_module "forgejo.org/modules/webhook" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -func TestCreateScheduleTask(t *testing.T) { - require.NoError(t, unittest.PrepareTestDatabase()) - repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 2, OwnerID: 2}) - - assertConstant := func(t *testing.T, cron *actions_model.ActionSchedule, run *actions_model.ActionRun) { - t.Helper() - assert.Equal(t, cron.Title, run.Title) - assert.Equal(t, cron.RepoID, run.RepoID) - assert.Equal(t, cron.OwnerID, run.OwnerID) - assert.Equal(t, cron.WorkflowID, run.WorkflowID) - assert.Equal(t, cron.TriggerUserID, run.TriggerUserID) - assert.Equal(t, cron.Ref, run.Ref) - assert.Equal(t, cron.CommitSHA, run.CommitSHA) - assert.Equal(t, cron.Event, run.Event) - assert.Equal(t, cron.EventPayload, run.EventPayload) - assert.Equal(t, cron.ID, run.ScheduleID) - assert.Equal(t, actions_model.StatusWaiting, run.Status) - } - - assertMutable := func(t *testing.T, expected, run *actions_model.ActionRun) { - t.Helper() - assert.Equal(t, expected.NotifyEmail, run.NotifyEmail) - } - - testCases := []struct { - name string - cron actions_model.ActionSchedule - want []actions_model.ActionRun - }{ - { - name: "simple", - cron: actions_model.ActionSchedule{ - Title: "scheduletitle1", - RepoID: repo.ID, - OwnerID: repo.OwnerID, - WorkflowID: "some.yml", - TriggerUserID: repo.OwnerID, - Ref: "branch", - CommitSHA: "fakeSHA", - Event: webhook_module.HookEventSchedule, - EventPayload: "fakepayload", - Content: []byte( - ` -name: test -on: push -jobs: - job2: - runs-on: ubuntu-latest - steps: - - run: true -`), - }, - want: []actions_model.ActionRun{ - { - Title: "scheduletitle1", - NotifyEmail: false, - }, - }, - }, - { - name: "enable-email-notifications is true", - cron: actions_model.ActionSchedule{ - Title: "scheduletitle2", - RepoID: repo.ID, - OwnerID: repo.OwnerID, - WorkflowID: "some.yml", - TriggerUserID: repo.OwnerID, - Ref: "branch", - CommitSHA: "fakeSHA", - Event: webhook_module.HookEventSchedule, - EventPayload: "fakepayload", - Content: []byte( - ` -name: test -enable-email-notifications: true -on: push -jobs: - job2: - runs-on: ubuntu-latest - steps: - - run: true -`), - }, - want: []actions_model.ActionRun{ - { - Title: "scheduletitle2", - NotifyEmail: true, - }, - }, - }, - } - for _, testCase := range testCases { - t.Run(testCase.name, func(t *testing.T) { - require.NoError(t, CreateScheduleTask(t.Context(), &testCase.cron)) - require.Equal(t, len(testCase.want), unittest.GetCount(t, actions_model.ActionRun{RepoID: repo.ID})) - for _, expected := range testCase.want { - run := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRun{Title: expected.Title}) - assertConstant(t, &testCase.cron, run) - assertMutable(t, &expected, run) - } - unittest.AssertSuccessfulDelete(t, actions_model.ActionRun{RepoID: repo.ID}) - }) - } -} diff --git a/services/actions/workflows.go b/services/actions/workflows.go index fbba3fd667..7ec7c3abed 100644 --- a/services/actions/workflows.go +++ b/services/actions/workflows.go @@ -111,11 +111,6 @@ func (entry *Workflow) Dispatch(ctx context.Context, inputGetter InputValueGette return nil, nil, err } - notifications, err := wf.Notifications() - if err != nil { - return nil, nil, err - } - run := &actions_model.ActionRun{ Title: title, RepoID: repo.ID, @@ -130,7 +125,6 @@ func (entry *Workflow) Dispatch(ctx context.Context, inputGetter InputValueGette EventPayload: string(p), TriggerEvent: string(webhook.HookEventWorkflowDispatch), Status: actions_model.StatusWaiting, - NotifyEmail: notifications, } vars, err := actions_model.GetVariablesOfRun(ctx, run) diff --git a/services/convert/action.go b/services/convert/action.go index 703c1f1261..5e17172b45 100644 --- a/services/convert/action.go +++ b/services/convert/action.go @@ -8,17 +8,22 @@ import ( actions_model "forgejo.org/models/actions" access_model "forgejo.org/models/perm/access" - user_model "forgejo.org/models/user" api "forgejo.org/modules/structs" ) // ToActionRun convert actions_model.User to api.ActionRun // the run needs all attributes loaded -func ToActionRun(ctx context.Context, run *actions_model.ActionRun, doer *user_model.User) *api.ActionRun { +func ToActionRun(ctx context.Context, run *actions_model.ActionRun) *api.ActionRun { if run == nil { return nil } + // The doer is the one whose perspective is used to view this ActionRun. + // In the best case we use the user that created the webhook. + // Unfortunately we don't know who that was. + // So instead we use the repo owner, who is able to create webhooks and allow others to do so by making them repo admins. + // This is pretty close to perfect. + doer := run.Repo.Owner permissionInRepo, _ := access_model.GetUserRepoPermission(ctx, run.Repo, doer) return &api.ActionRun{ diff --git a/services/convert/convert.go b/services/convert/convert.go index 2ea24a1b51..48da9d7623 100644 --- a/services/convert/convert.go +++ b/services/convert/convert.go @@ -222,6 +222,29 @@ func ToActionTask(ctx context.Context, t *actions_model.ActionTask) (*api.Action }, nil } +// ToRepoActionRun convert a actions_model.ActionRun to an api.RepoActionRun +func ToRepoActionRun(ctx context.Context, r *actions_model.ActionRun) (*api.RepoActionRun, error) { + if err := r.LoadAttributes(ctx); err != nil { + return nil, err + } + + url := strings.TrimSuffix(setting.AppURL, "/") + r.Link() + actor := ToUser(ctx, r.TriggerUser, nil) + + return &api.RepoActionRun{ + ID: r.ID, + Name: r.Title, + HeadBranch: r.PrettyRef(), + HeadSHA: r.CommitSHA, + RunNumber: r.Index, + Event: r.TriggerEvent, + Status: r.Status.String(), + WorkflowID: r.WorkflowID, + URL: url, + TriggeringActor: actor, + }, nil +} + // ToVerification convert a git.Commit.Signature to an api.PayloadCommitVerification func ToVerification(ctx context.Context, c *git.Commit) *api.PayloadCommitVerification { verif := asymkey_model.ParseCommitWithSignature(ctx, c) diff --git a/services/federation/federation_service.go b/services/federation/federation_service.go index a3b719d1a7..174c175f86 100644 --- a/services/federation/federation_service.go +++ b/services/federation/federation_service.go @@ -211,11 +211,6 @@ func CreateUserFromAP(ctx context.Context, personID fm.PersonID, federationHostI return nil, nil, err } - inbox, err := url.ParseRequestURI(person.Inbox.GetLink().String()) - if err != nil { - return nil, nil, err - } - newUser := user.User{ LowerName: strings.ToLower(name), Name: name, @@ -232,7 +227,6 @@ func CreateUserFromAP(ctx context.Context, personID fm.PersonID, federationHostI federatedUser := user.FederatedUser{ ExternalID: personID.ID, FederationHostID: federationHostID, - InboxPath: inbox.Path, NormalizedOriginalURL: personID.AsURI(), } diff --git a/services/feed/action.go b/services/feed/action.go index 7d179bd1c8..a2cd0551a3 100644 --- a/services/feed/action.go +++ b/services/feed/action.go @@ -39,24 +39,6 @@ func NewNotifier() notify_service.Notifier { return &actionNotifier{} } -func notifyAll(ctx context.Context, action *activities_model.Action) error { - _, err := activities_model.NotifyWatchers(ctx, action) - if err != nil { - return err - } - return err - // return federation_service.NotifyActivityPubFollowers(ctx, out) -} - -func notifyAllActions(ctx context.Context, acts []*activities_model.Action) error { - _, err := activities_model.NotifyWatchersActions(ctx, acts) - if err != nil { - return err - } - return nil - // return federation_service.NotifyActivityPubFollowers(ctx, out) -} - func (a *actionNotifier) NewIssue(ctx context.Context, issue *issues_model.Issue, mentions []*user_model.User) { if err := issue.LoadPoster(ctx); err != nil { log.Error("issue.LoadPoster: %v", err) @@ -68,7 +50,7 @@ func (a *actionNotifier) NewIssue(ctx context.Context, issue *issues_model.Issue } repo := issue.Repo - if err := notifyAll(ctx, &activities_model.Action{ + if err := activities_model.NotifyWatchers(ctx, &activities_model.Action{ ActUserID: issue.Poster.ID, ActUser: issue.Poster, OpType: activities_model.ActionCreateIssue, @@ -109,7 +91,7 @@ func (a *actionNotifier) IssueChangeStatus(ctx context.Context, doer *user_model } // Notify watchers for whatever action comes in, ignore if no action type. - if err := notifyAll(ctx, act); err != nil { + if err := activities_model.NotifyWatchers(ctx, act); err != nil { log.Error("NotifyWatchers: %v", err) } } @@ -145,7 +127,7 @@ func (a *actionNotifier) CreateIssueComment(ctx context.Context, doer *user_mode } // Notify watchers for whatever action comes in, ignore if no action type. - if err := notifyAll(ctx, act); err != nil { + if err := activities_model.NotifyWatchers(ctx, act); err != nil { log.Error("NotifyWatchers: %v", err) } } @@ -164,7 +146,7 @@ func (a *actionNotifier) NewPullRequest(ctx context.Context, pull *issues_model. return } - if err := notifyAll(ctx, &activities_model.Action{ + if err := activities_model.NotifyWatchers(ctx, &activities_model.Action{ ActUserID: pull.Issue.Poster.ID, ActUser: pull.Issue.Poster, OpType: activities_model.ActionCreatePullRequest, @@ -178,7 +160,7 @@ func (a *actionNotifier) NewPullRequest(ctx context.Context, pull *issues_model. } func (a *actionNotifier) RenameRepository(ctx context.Context, doer *user_model.User, repo *repo_model.Repository, oldRepoName string) { - if err := notifyAll(ctx, &activities_model.Action{ + if err := activities_model.NotifyWatchers(ctx, &activities_model.Action{ ActUserID: doer.ID, ActUser: doer, OpType: activities_model.ActionRenameRepo, @@ -192,7 +174,7 @@ func (a *actionNotifier) RenameRepository(ctx context.Context, doer *user_model. } func (a *actionNotifier) TransferRepository(ctx context.Context, doer *user_model.User, repo *repo_model.Repository, oldOwnerName string) { - if err := notifyAll(ctx, &activities_model.Action{ + if err := activities_model.NotifyWatchers(ctx, &activities_model.Action{ ActUserID: doer.ID, ActUser: doer, OpType: activities_model.ActionTransferRepo, @@ -206,7 +188,7 @@ func (a *actionNotifier) TransferRepository(ctx context.Context, doer *user_mode } func (a *actionNotifier) CreateRepository(ctx context.Context, doer, u *user_model.User, repo *repo_model.Repository) { - if err := notifyAll(ctx, &activities_model.Action{ + if err := activities_model.NotifyWatchers(ctx, &activities_model.Action{ ActUserID: doer.ID, ActUser: doer, OpType: activities_model.ActionCreateRepo, @@ -219,7 +201,7 @@ func (a *actionNotifier) CreateRepository(ctx context.Context, doer, u *user_mod } func (a *actionNotifier) ForkRepository(ctx context.Context, doer *user_model.User, oldRepo, repo *repo_model.Repository) { - if err := notifyAll(ctx, &activities_model.Action{ + if err := activities_model.NotifyWatchers(ctx, &activities_model.Action{ ActUserID: doer.ID, ActUser: doer, OpType: activities_model.ActionCreateRepo, @@ -284,13 +266,13 @@ func (a *actionNotifier) PullRequestReview(ctx context.Context, pr *issues_model actions = append(actions, action) } - if err := notifyAllActions(ctx, actions); err != nil { + if err := activities_model.NotifyWatchersActions(ctx, actions); err != nil { log.Error("notify watchers '%d/%d': %v", review.Reviewer.ID, review.Issue.RepoID, err) } } func (*actionNotifier) MergePullRequest(ctx context.Context, doer *user_model.User, pr *issues_model.PullRequest) { - if err := notifyAll(ctx, &activities_model.Action{ + if err := activities_model.NotifyWatchers(ctx, &activities_model.Action{ ActUserID: doer.ID, ActUser: doer, OpType: activities_model.ActionMergePullRequest, @@ -304,7 +286,7 @@ func (*actionNotifier) MergePullRequest(ctx context.Context, doer *user_model.Us } func (*actionNotifier) AutoMergePullRequest(ctx context.Context, doer *user_model.User, pr *issues_model.PullRequest) { - if err := notifyAll(ctx, &activities_model.Action{ + if err := activities_model.NotifyWatchers(ctx, &activities_model.Action{ ActUserID: doer.ID, ActUser: doer, OpType: activities_model.ActionAutoMergePullRequest, @@ -322,7 +304,7 @@ func (*actionNotifier) NotifyPullRevieweDismiss(ctx context.Context, doer *user_ if len(review.OriginalAuthor) > 0 { reviewerName = review.OriginalAuthor } - if err := notifyAll(ctx, &activities_model.Action{ + if err := activities_model.NotifyWatchers(ctx, &activities_model.Action{ ActUserID: doer.ID, ActUser: doer, OpType: activities_model.ActionPullReviewDismissed, @@ -360,7 +342,7 @@ func (a *actionNotifier) PushCommits(ctx context.Context, pusher *user_model.Use opType = activities_model.ActionDeleteBranch } - if err = notifyAll(ctx, &activities_model.Action{ + if err = activities_model.NotifyWatchers(ctx, &activities_model.Action{ ActUserID: pusher.ID, ActUser: pusher, OpType: opType, @@ -380,7 +362,7 @@ func (a *actionNotifier) CreateRef(ctx context.Context, doer *user_model.User, r // has sent same action in `PushCommits`, so skip it. return } - if err := notifyAll(ctx, &activities_model.Action{ + if err := activities_model.NotifyWatchers(ctx, &activities_model.Action{ ActUserID: doer.ID, ActUser: doer, OpType: opType, @@ -399,7 +381,7 @@ func (a *actionNotifier) DeleteRef(ctx context.Context, doer *user_model.User, r // has sent same action in `PushCommits`, so skip it. return } - if err := notifyAll(ctx, &activities_model.Action{ + if err := activities_model.NotifyWatchers(ctx, &activities_model.Action{ ActUserID: doer.ID, ActUser: doer, OpType: opType, @@ -423,7 +405,7 @@ func (a *actionNotifier) SyncPushCommits(ctx context.Context, pusher *user_model return } - if err := notifyAll(ctx, &activities_model.Action{ + if err := activities_model.NotifyWatchers(ctx, &activities_model.Action{ ActUserID: repo.OwnerID, ActUser: repo.MustOwner(ctx), OpType: activities_model.ActionMirrorSyncPush, @@ -438,7 +420,7 @@ func (a *actionNotifier) SyncPushCommits(ctx context.Context, pusher *user_model } func (a *actionNotifier) SyncCreateRef(ctx context.Context, doer *user_model.User, repo *repo_model.Repository, refFullName git.RefName, refID string) { - if err := notifyAll(ctx, &activities_model.Action{ + if err := activities_model.NotifyWatchers(ctx, &activities_model.Action{ ActUserID: repo.OwnerID, ActUser: repo.MustOwner(ctx), OpType: activities_model.ActionMirrorSyncCreate, @@ -452,7 +434,7 @@ func (a *actionNotifier) SyncCreateRef(ctx context.Context, doer *user_model.Use } func (a *actionNotifier) SyncDeleteRef(ctx context.Context, doer *user_model.User, repo *repo_model.Repository, refFullName git.RefName) { - if err := notifyAll(ctx, &activities_model.Action{ + if err := activities_model.NotifyWatchers(ctx, &activities_model.Action{ ActUserID: repo.OwnerID, ActUser: repo.MustOwner(ctx), OpType: activities_model.ActionMirrorSyncDelete, @@ -470,7 +452,7 @@ func (a *actionNotifier) NewRelease(ctx context.Context, rel *repo_model.Release log.Error("LoadAttributes: %v", err) return } - if err := notifyAll(ctx, &activities_model.Action{ + if err := activities_model.NotifyWatchers(ctx, &activities_model.Action{ ActUserID: rel.PublisherID, ActUser: rel.Publisher, OpType: activities_model.ActionPublishRelease, diff --git a/services/issue/pull.go b/services/issue/pull.go index 2eef1fbfa8..b0a0c47d88 100644 --- a/services/issue/pull.go +++ b/services/issue/pull.go @@ -43,6 +43,8 @@ type ReviewRequestNotifier struct { } func PullRequestCodeOwnersReview(ctx context.Context, issue *issues_model.Issue, pr *issues_model.PullRequest) ([]*ReviewRequestNotifier, error) { + files := []string{"CODEOWNERS", "docs/CODEOWNERS", ".gitea/CODEOWNERS"} + if pr.IsWorkInProgress(ctx) { return nil, nil } @@ -70,17 +72,18 @@ func PullRequestCodeOwnersReview(ctx context.Context, issue *issues_model.Issue, return nil, err } - var rules []*issues_model.CodeOwnerRule - for _, file := range []string{"CODEOWNERS", "docs/CODEOWNERS", ".gitea/CODEOWNERS"} { + var data string + for _, file := range files { if blob, err := commit.GetBlobByPath(file); err == nil { - rc, size, err := blob.NewTruncatedReader(setting.UI.MaxDisplayFileSize) + data, err = blob.GetBlobContent(setting.UI.MaxDisplayFileSize) if err == nil { - rules, _ = issues_model.GetCodeOwnersFromReader(ctx, rc, size > setting.UI.MaxDisplayFileSize) break } } } + rules, _ := issues_model.GetCodeOwnersFromContent(ctx, data) + // get the mergebase mergeBase, err := getMergeBase(repo, pr, git.BranchPrefix+pr.BaseBranch, pr.GetGitRefName()) if err != nil { diff --git a/services/mailer/mail_actions.go b/services/mailer/mail_actions.go index 09763e164e..7c63603a98 100644 --- a/services/mailer/mail_actions.go +++ b/services/mailer/mail_actions.go @@ -23,24 +23,19 @@ func MailActionRun(run *actions_model.ActionRun, priorStatus actions_model.Statu return nil } - if !run.NotifyEmail { - return nil + if run.TriggerUser.Email != "" && run.TriggerUser.EmailNotificationsPreference != user_model.EmailNotificationsDisabled { + if err := sendMailActionRun(run.TriggerUser, run, priorStatus, lastRun); err != nil { + return err + } } - user := run.TriggerUser - // this happens e.g. when this is a scheduled run - if user.IsSystem() { - user = run.Repo.Owner - } - if user.IsSystem() || user.Email == "" { - return nil + if run.Repo.Owner.Email != "" && run.Repo.Owner.Email != run.TriggerUser.Email && run.Repo.Owner.EmailNotificationsPreference != user_model.EmailNotificationsDisabled { + if err := sendMailActionRun(run.Repo.Owner, run, priorStatus, lastRun); err != nil { + return err + } } - if user.EmailNotificationsPreference == user_model.EmailNotificationsDisabled { - return nil - } - - return sendMailActionRun(user, run, priorStatus, lastRun) + return nil } func sendMailActionRun(to *user_model.User, run *actions_model.ActionRun, priorStatus actions_model.Status, lastRun *actions_model.ActionRun) error { diff --git a/services/mailer/mail_actions_now_done_test.go b/services/mailer/mail_actions_now_done_test.go index 6a01ea7631..0d832f2b36 100644 --- a/services/mailer/mail_actions_now_done_test.go +++ b/services/mailer/mail_actions_now_done_test.go @@ -4,53 +4,42 @@ package mailer import ( - "slices" "testing" actions_model "forgejo.org/models/actions" "forgejo.org/models/db" - organization_model "forgejo.org/models/organization" repo_model "forgejo.org/models/repo" user_model "forgejo.org/models/user" - "forgejo.org/modules/optional" "forgejo.org/modules/setting" - "forgejo.org/modules/test" notify_service "forgejo.org/services/notify" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) -func getActionsNowDoneTestUser(t *testing.T, name, email, notifications string) *user_model.User { +func getActionsNowDoneTestUsers(t *testing.T) []*user_model.User { t.Helper() - user := new(user_model.User) - user.Name = name - user.Language = "en_US" - user.IsAdmin = false - user.Email = email - user.LastLoginUnix = 1693648327 - user.CreatedUnix = 1693648027 - opts := user_model.CreateUserOverwriteOptions{ - AllowCreateOrganization: optional.Some(true), - EmailNotificationsPreference: ¬ifications, - } - require.NoError(t, user_model.AdminCreateUser(db.DefaultContext, user, &opts)) - return user -} + newTriggerUser := new(user_model.User) + newTriggerUser.Name = "new_trigger_user" + newTriggerUser.Language = "en_US" + newTriggerUser.IsAdmin = false + newTriggerUser.Email = "new_trigger_user@example.com" + newTriggerUser.LastLoginUnix = 1693648327 + newTriggerUser.CreatedUnix = 1693648027 + newTriggerUser.EmailNotificationsPreference = user_model.EmailNotificationsEnabled + require.NoError(t, user_model.CreateUser(db.DefaultContext, newTriggerUser)) -func getActionsNowDoneTestOrg(t *testing.T, name, email string, owner *user_model.User) *user_model.User { - t.Helper() - org := new(organization_model.Organization) - org.Name = name - org.Language = "en_US" - org.IsAdmin = false - // contact email for the organization, for display purposes but otherwise not used as of v12 - org.Email = email - org.LastLoginUnix = 1693648327 - org.CreatedUnix = 1693648027 - org.Email = email - require.NoError(t, organization_model.CreateOrganization(db.DefaultContext, org, owner)) - return (*user_model.User)(org) + newOwner := new(user_model.User) + newOwner.Name = "new_owner" + newOwner.Language = "en_US" + newOwner.IsAdmin = false + newOwner.Email = "new_owner@example.com" + newOwner.LastLoginUnix = 1693648329 + newOwner.CreatedUnix = 1693648029 + newOwner.EmailNotificationsPreference = user_model.EmailNotificationsEnabled + require.NoError(t, user_model.CreateUser(db.DefaultContext, newOwner)) + + return []*user_model.User{newTriggerUser, newOwner} } func assertTranslatedLocaleMailActionsNowDone(t *testing.T, msgBody string) { @@ -60,181 +49,98 @@ func assertTranslatedLocaleMailActionsNowDone(t *testing.T, msgBody string) { func TestActionRunNowDoneNotificationMail(t *testing.T) { ctx := t.Context() - defer test.MockVariableValue(&setting.Admin.DisableRegularOrgCreation, false)() - - actionsUser := user_model.NewActionsUser() - require.NotEmpty(t, actionsUser.Email) + users := getActionsNowDoneTestUsers(t) + defer CleanUpUsers(ctx, users) + triggerUser := users[0] + ownerUser := users[1] repo := repo_model.Repository{ Name: "some repo", Description: "rockets are cool", + Owner: ownerUser, + OwnerID: ownerUser.ID, } // Do some funky stuff with the action run's ids: // The run with the larger ID finished first. // This is odd but something that must work. - run1 := &actions_model.ActionRun{ID: 2, Repo: &repo, RepoID: repo.ID, Title: "some workflow", Status: actions_model.StatusFailure, Stopped: 1745821796, TriggerEvent: "workflow_dispatch"} - run2 := &actions_model.ActionRun{ID: 1, Repo: &repo, RepoID: repo.ID, Title: "some workflow", Status: actions_model.StatusSuccess, Stopped: 1745822796, TriggerEvent: "push"} - - assignUsers := func(triggerUser, owner *user_model.User) { - for _, run := range []*actions_model.ActionRun{run1, run2} { - run.TriggerUser = triggerUser - run.TriggerUserID = triggerUser.ID - run.NotifyEmail = true - } - repo.Owner = owner - repo.OwnerID = owner.ID - } + run1 := &actions_model.ActionRun{ID: 2, Repo: &repo, RepoID: repo.ID, Title: "some workflow", TriggerUser: triggerUser, TriggerUserID: triggerUser.ID, Status: actions_model.StatusFailure, Stopped: 1745821796, TriggerEvent: "workflow_dispatch"} + run2 := &actions_model.ActionRun{ID: 1, Repo: &repo, RepoID: repo.ID, Title: "some workflow", TriggerUser: triggerUser, TriggerUserID: triggerUser.ID, Status: actions_model.StatusSuccess, Stopped: 1745822796, TriggerEvent: "push"} notify_service.RegisterNotifier(NewNotifier()) - orgOwner := getActionsNowDoneTestUser(t, "org_owner", "org_owner@example.com", "disabled") - defer CleanUpUsers(ctx, []*user_model.User{orgOwner}) - t.Run("DontSendNotificationEmailOnFirstActionSuccess", func(t *testing.T) { - user := getActionsNowDoneTestUser(t, "new_user", "new_user@example.com", "enabled") - defer CleanUpUsers(ctx, []*user_model.User{user}) - assignUsers(user, user) defer MockMailSettings(func(msgs ...*Message) { assert.Fail(t, "no mail should be sent") })() notify_service.ActionRunNowDone(ctx, run2, actions_model.StatusRunning, nil) }) - t.Run("WorkflowEnableEmailNotificationIsFalse", func(t *testing.T) { - user := getActionsNowDoneTestUser(t, "new_user1", "new_user1@example.com", "enabled") - defer CleanUpUsers(ctx, []*user_model.User{user}) - assignUsers(user, user) + t.Run("SendNotificationEmailOnActionRunFailed", func(t *testing.T) { + mailSentToOwner := false + mailSentToTriggerUser := false defer MockMailSettings(func(msgs ...*Message) { - assert.Fail(t, "no mail should be sent") + assert.LessOrEqual(t, len(msgs), 2) + for _, msg := range msgs { + switch msg.To { + case triggerUser.EmailTo(): + assert.False(t, mailSentToTriggerUser, "sent mail twice") + mailSentToTriggerUser = true + case ownerUser.EmailTo(): + assert.False(t, mailSentToOwner, "sent mail twice") + mailSentToOwner = true + default: + assert.Fail(t, "sent mail to unknown sender", msg.To) + } + assert.Contains(t, msg.Body, triggerUser.HTMLURL()) + assert.Contains(t, msg.Body, triggerUser.Name) + // what happened + assert.Contains(t, msg.Body, "failed") + // new status of run + assert.Contains(t, msg.Body, "failure") + // prior status of this run + assert.Contains(t, msg.Body, "waiting") + assertTranslatedLocaleMailActionsNowDone(t, msg.Body) + } })() - run2.NotifyEmail = false - notify_service.ActionRunNowDone(ctx, run2, actions_model.StatusRunning, nil) + notify_service.ActionRunNowDone(ctx, run1, actions_model.StatusWaiting, nil) + assert.True(t, mailSentToOwner) + assert.True(t, mailSentToTriggerUser) }) - for _, testCase := range []struct { - name string - triggerUser *user_model.User - owner *user_model.User - expected string - expectMail bool - }{ - { - // if the action is assigned a trigger user in a repository - // owned by a regular user, the mail is sent to the trigger user - name: "RegularTriggerUser", - triggerUser: getActionsNowDoneTestUser(t, "new_trigger_user0", "new_trigger_user0@example.com", user_model.EmailNotificationsEnabled), - owner: getActionsNowDoneTestUser(t, "new_owner0", "new_owner0@example.com", user_model.EmailNotificationsEnabled), - expected: "trigger", - expectMail: true, - }, - { - // if the action is assigned to a system user (e.g. ActionsUser) - // in a repository owned by a regular user, the mail is sent to - // the user that owns the repository - name: "SystemTriggerUserAndRegularOwner", - triggerUser: actionsUser, - owner: getActionsNowDoneTestUser(t, "new_owner1", "new_owner1@example.com", user_model.EmailNotificationsEnabled), - expected: "owner", - expectMail: true, - }, - { - // if the action is assigned a trigger user with disabled notifications in a repository - // owned by a regular user, no mail is sent - name: "RegularTriggerUserNotificationsDisabled", - triggerUser: getActionsNowDoneTestUser(t, "new_trigger_user2", "new_trigger_user2@example.com", user_model.EmailNotificationsDisabled), - owner: getActionsNowDoneTestUser(t, "new_owner2", "new_owner2@example.com", user_model.EmailNotificationsEnabled), - expectMail: false, - }, - { - // if the action is assigned to a system user (e.g. ActionsUser) - // owned by a regular user with disabled notifications, no mail is sent - name: "SystemTriggerUserAndRegularOwnerNotificationsDisabled", - triggerUser: actionsUser, - owner: getActionsNowDoneTestUser(t, "new_owner3", "new_owner3@example.com", user_model.EmailNotificationsDisabled), - expectMail: false, - }, - { - // if the action is assigned to a system user (e.g. ActionsUser) - // in a repository owned by an organization with an email contact, the mail is sent to - // this email contact - name: "SystemTriggerUserAndOrgOwner", - triggerUser: actionsUser, - owner: getActionsNowDoneTestOrg(t, "new_org1", "new_org_owner0@example.com", orgOwner), - expected: "owner", - expectMail: true, - }, - { - // if the action is assigned to a system user (e.g. ActionsUser) - // in a repository owned by an organization without an email contact, no mail is sent - name: "SystemTriggerUserAndNoMailOrgOwner", - triggerUser: actionsUser, - owner: getActionsNowDoneTestOrg(t, "new_org2", "", orgOwner), - expectMail: false, - }, - } { - t.Run(testCase.name, func(t *testing.T) { - assignUsers(testCase.triggerUser, testCase.owner) - defer CleanUpUsers(ctx, slices.DeleteFunc([]*user_model.User{testCase.triggerUser, testCase.owner}, func(user *user_model.User) bool { - return user.IsSystem() - })) + t.Run("SendNotificationEmailOnActionRunRecovered", func(t *testing.T) { + mailSentToOwner := false + mailSentToTriggerUser := false + defer MockMailSettings(func(msgs ...*Message) { + assert.LessOrEqual(t, len(msgs), 2) + for _, msg := range msgs { + switch msg.To { + case triggerUser.EmailTo(): + assert.False(t, mailSentToTriggerUser, "sent mail twice") + mailSentToTriggerUser = true + case ownerUser.EmailTo(): + assert.False(t, mailSentToOwner, "sent mail twice") + mailSentToOwner = true + default: + assert.Fail(t, "sent mail to unknown sender", msg.To) + } + assert.Contains(t, msg.Body, triggerUser.HTMLURL()) + assert.Contains(t, msg.Body, triggerUser.Name) + // what happened + assert.Contains(t, msg.Body, "recovered") + // old status of run + assert.Contains(t, msg.Body, "failure") + // new status of run + assert.Contains(t, msg.Body, "success") + // prior status of this run + assert.Contains(t, msg.Body, "running") + assertTranslatedLocaleMailActionsNowDone(t, msg.Body) + } + })() + assert.NotNil(t, setting.MailService) - t.Run("SendNotificationEmailOnActionRunFailed", func(t *testing.T) { - mailSent := false - defer MockMailSettings(func(msgs ...*Message) { - assert.Len(t, msgs, 1) - msg := msgs[0] - assert.False(t, mailSent, "sent mail twice") - expectedEmail := testCase.triggerUser.Email - if testCase.expected == "owner" { // otherwise "trigger" - expectedEmail = testCase.owner.Email - } - require.Contains(t, msg.To, expectedEmail, "sent mail to unknown sender") - mailSent = true - assert.Contains(t, msg.Body, testCase.triggerUser.HTMLURL()) - assert.Contains(t, msg.Body, testCase.triggerUser.Name) - // what happened - assert.Contains(t, msg.Body, "failed") - // new status of run - assert.Contains(t, msg.Body, "failure") - // prior status of this run - assert.Contains(t, msg.Body, "waiting") - assertTranslatedLocaleMailActionsNowDone(t, msg.Body) - })() - require.NotNil(t, setting.MailService) - - notify_service.ActionRunNowDone(ctx, run1, actions_model.StatusWaiting, nil) - assert.Equal(t, testCase.expectMail, mailSent) - }) - - t.Run("SendNotificationEmailOnActionRunRecovered", func(t *testing.T) { - mailSent := false - defer MockMailSettings(func(msgs ...*Message) { - assert.Len(t, msgs, 1) - msg := msgs[0] - assert.False(t, mailSent, "sent mail twice") - expectedEmail := testCase.triggerUser.Email - if testCase.expected == "owner" { // otherwise "trigger" - expectedEmail = testCase.owner.Email - } - require.Contains(t, msg.To, expectedEmail, "sent mail to unknown sender") - mailSent = true - assert.Contains(t, msg.Body, testCase.triggerUser.HTMLURL()) - assert.Contains(t, msg.Body, testCase.triggerUser.Name) - // what happened - assert.Contains(t, msg.Body, "recovered") - // old status of run - assert.Contains(t, msg.Body, "failure") - // new status of run - assert.Contains(t, msg.Body, "success") - // prior status of this run - assert.Contains(t, msg.Body, "running") - })() - require.NotNil(t, setting.MailService) - - notify_service.ActionRunNowDone(ctx, run2, actions_model.StatusRunning, run1) - assert.Equal(t, testCase.expectMail, mailSent) - }) - }) - } + notify_service.ActionRunNowDone(ctx, run2, actions_model.StatusRunning, run1) + assert.True(t, mailSentToOwner) + assert.True(t, mailSentToTriggerUser) + }) } diff --git a/services/mailer/main_test.go b/services/mailer/main_test.go index 5e9cbe3e99..47e5d5d175 100644 --- a/services/mailer/main_test.go +++ b/services/mailer/main_test.go @@ -8,7 +8,6 @@ import ( "testing" "forgejo.org/models/db" - organization_model "forgejo.org/models/organization" "forgejo.org/models/unittest" user_model "forgejo.org/models/user" "forgejo.org/modules/setting" @@ -52,11 +51,6 @@ func MockMailSettings(send func(msgs ...*Message)) func() { func CleanUpUsers(ctx context.Context, users []*user_model.User) { for _, u := range users { - if u.IsOrganization() { - organization_model.DeleteOrganization(ctx, (*organization_model.Organization)(u)) - } else { - db.DeleteByID[user_model.User](ctx, u.ID) - db.DeleteByBean(ctx, &user_model.EmailAddress{UID: u.ID}) - } + db.DeleteByID[user_model.User](ctx, u.ID) } } diff --git a/services/webhook/notifier.go b/services/webhook/notifier.go index 009efc994f..b3201e5d10 100644 --- a/services/webhook/notifier.go +++ b/services/webhook/notifier.go @@ -894,16 +894,9 @@ func (m *webhookNotifier) ActionRunNowDone(ctx context.Context, run *actions_mod Owner: run.TriggerUser, } - // The doer is the one whose perspective is used to view this ActionRun. - // In the best case we use the user that created the webhook. - // Unfortunately we don't know who that was. - // So instead we use the repo owner, who is able to create webhooks and allow others to do so by making them repo admins. - // This is pretty close to perfect. - doer := run.Repo.Owner - payload := &api.ActionPayload{ - Run: convert.ToActionRun(ctx, run, doer), - LastRun: convert.ToActionRun(ctx, lastRun, doer), + Run: convert.ToActionRun(ctx, run), + LastRun: convert.ToActionRun(ctx, lastRun), PriorStatus: priorStatus.String(), } diff --git a/templates/repo/issue/view_content/comments.tmpl b/templates/repo/issue/view_content/comments.tmpl index aca77a92e5..9eb9307f9f 100644 --- a/templates/repo/issue/view_content/comments.tmpl +++ b/templates/repo/issue/view_content/comments.tmpl @@ -1,7 +1,7 @@ {{template "base/alert"}} {{range .Issue.Comments}} {{if call $.ShouldShowCommentType .Type}} - {{$createdStr := HTMLFormat `%s` .EventTag .HashTag (DateUtils.TimeSince .CreatedUnix)}} + {{$createdStr:= DateUtils.TimeSince .CreatedUnix}}