mirror of
https://codeberg.org/davrot/forgejo.git
synced 2025-07-04 07:00:02 +02:00
Merge branch 'forgejo' into upload_with_path_structure
Some checks failed
Integration tests for the release process / release-simulation (push) Has been cancelled
Some checks failed
Integration tests for the release process / release-simulation (push) Has been cancelled
This commit is contained in:
commit
c3b559b79b
82 changed files with 2730 additions and 512 deletions
|
@ -13,6 +13,13 @@ forgejo.org/models
|
||||||
IsErrSHANotFound
|
IsErrSHANotFound
|
||||||
IsErrMergeDivergingFastForwardOnly
|
IsErrMergeDivergingFastForwardOnly
|
||||||
|
|
||||||
|
forgejo.org/models/activities
|
||||||
|
GetActivityByID
|
||||||
|
NewFederatedUserActivity
|
||||||
|
CreateUserActivity
|
||||||
|
GetFollowingFeeds
|
||||||
|
FederatedUserActivity.loadActor
|
||||||
|
|
||||||
forgejo.org/models/auth
|
forgejo.org/models/auth
|
||||||
WebAuthnCredentials
|
WebAuthnCredentials
|
||||||
|
|
||||||
|
@ -54,9 +61,17 @@ forgejo.org/models/user
|
||||||
IsErrExternalLoginUserAlreadyExist
|
IsErrExternalLoginUserAlreadyExist
|
||||||
IsErrExternalLoginUserNotExist
|
IsErrExternalLoginUserNotExist
|
||||||
NewFederatedUser
|
NewFederatedUser
|
||||||
|
NewFederatedUserFollower
|
||||||
IsErrUserSettingIsNotExist
|
IsErrUserSettingIsNotExist
|
||||||
GetUserAllSettings
|
GetUserAllSettings
|
||||||
DeleteUserSetting
|
DeleteUserSetting
|
||||||
|
GetFederatedUser
|
||||||
|
GetFederatedUserByUserID
|
||||||
|
UpdateFederatedUser
|
||||||
|
GetFollowersForUser
|
||||||
|
AddFollower
|
||||||
|
RemoveFollower
|
||||||
|
IsFollowingAp
|
||||||
|
|
||||||
forgejo.org/modules/activitypub
|
forgejo.org/modules/activitypub
|
||||||
NewContext
|
NewContext
|
||||||
|
|
|
@ -28,7 +28,7 @@ jobs:
|
||||||
|
|
||||||
runs-on: docker
|
runs-on: docker
|
||||||
container:
|
container:
|
||||||
image: data.forgejo.org/renovate/renovate:40.57.1
|
image: data.forgejo.org/renovate/renovate:41.1.4
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Load renovate repo cache
|
- name: Load renovate repo cache
|
||||||
|
|
|
@ -115,6 +115,11 @@ jobs:
|
||||||
run: |
|
run: |
|
||||||
su forgejo -c 'make deps-frontend frontend'
|
su forgejo -c 'make deps-frontend frontend'
|
||||||
- uses: ./.forgejo/workflows-composite/build-backend
|
- 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
|
- name: Get changed files
|
||||||
id: changed-files
|
id: changed-files
|
||||||
uses: https://data.forgejo.org/tj-actions/changed-files@v46
|
uses: https://data.forgejo.org/tj-actions/changed-files@v46
|
||||||
|
@ -127,6 +132,7 @@ jobs:
|
||||||
USE_REPO_TEST_DIR: 1
|
USE_REPO_TEST_DIR: 1
|
||||||
PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: 1
|
PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: 1
|
||||||
CHANGED_FILES: ${{steps.changed-files.outputs.all_changed_files}}
|
CHANGED_FILES: ${{steps.changed-files.outputs.all_changed_files}}
|
||||||
|
RUN_ALL: ${{steps.run-all.all}}
|
||||||
- name: Upload test artifacts on failure
|
- name: Upload test artifacts on failure
|
||||||
if: failure()
|
if: failure()
|
||||||
uses: https://data.forgejo.org/forgejo/upload-artifact@v4
|
uses: https://data.forgejo.org/forgejo/upload-artifact@v4
|
||||||
|
|
2
Makefile
2
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
|
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
|
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
|
GOMOCK_PACKAGE ?= go.uber.org/mock/mockgen@v0.5.2 # renovate: datasource=go
|
||||||
RENOVATE_NPM_PACKAGE ?= renovate@40.57.1 # renovate: datasource=docker packageName=data.forgejo.org/renovate/renovate
|
RENOVATE_NPM_PACKAGE ?= renovate@41.1.4 # renovate: datasource=docker packageName=data.forgejo.org/renovate/renovate
|
||||||
|
|
||||||
# https://github.com/disposable-email-domains/disposable-email-domains/commits/main/
|
# https://github.com/disposable-email-domains/disposable-email-domains/commits/main/
|
||||||
DISPOSABLE_EMAILS_SHA ?= 0c27e671231d27cf66370034d7f6818037416989 # renovate: ...
|
DISPOSABLE_EMAILS_SHA ?= 0c27e671231d27cf66370034d7f6818037416989 # renovate: ...
|
||||||
|
|
2
go.mod
2
go.mod
|
@ -41,7 +41,7 @@ require (
|
||||||
github.com/gliderlabs/ssh v0.3.8
|
github.com/gliderlabs/ssh v0.3.8
|
||||||
github.com/go-ap/activitypub v0.0.0-20231114162308-e219254dc5c9
|
github.com/go-ap/activitypub v0.0.0-20231114162308-e219254dc5c9
|
||||||
github.com/go-ap/jsonld v0.0.0-20221030091449-f2a191312c73
|
github.com/go-ap/jsonld v0.0.0-20221030091449-f2a191312c73
|
||||||
github.com/go-chi/chi/v5 v5.2.1
|
github.com/go-chi/chi/v5 v5.2.2
|
||||||
github.com/go-chi/cors v1.2.1
|
github.com/go-chi/cors v1.2.1
|
||||||
github.com/go-co-op/gocron v1.37.0
|
github.com/go-co-op/gocron v1.37.0
|
||||||
github.com/go-enry/go-enry/v2 v2.9.2
|
github.com/go-enry/go-enry/v2 v2.9.2
|
||||||
|
|
4
go.sum
4
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 h1:MNHlNMBDgEKD4TcKr36vQN68BA00aDfjIt3/bD50WnA=
|
||||||
github.com/go-asn1-ber/asn1-ber v1.5.5/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0=
|
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.0.1/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8=
|
||||||
github.com/go-chi/chi/v5 v5.2.1 h1:KOIHODQj58PmL80G2Eak4WdvUzjSJSm0vG72crDCqb8=
|
github.com/go-chi/chi/v5 v5.2.2 h1:CMwsvRVTbXVytCk1Wd72Zy1LAsAh9GxMmSNWLHCG618=
|
||||||
github.com/go-chi/chi/v5 v5.2.1/go.mod h1:L2yAIGWB3H+phAw1NxKwWM+7eUH/lU8pOMm5hHcoops=
|
github.com/go-chi/chi/v5 v5.2.2/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 h1:xEC8UT3Rlp2QuWNEr4Fs/c2EAGVKBwy/1vHx3bppil4=
|
||||||
github.com/go-chi/cors v1.2.1/go.mod h1:sSbTewc+6wYHBBCW7ytsFSn836hqM7JxpglAy2Vzc58=
|
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=
|
github.com/go-co-op/gocron v1.37.0 h1:ZYDJGtQ4OMhTLKOKMIch+/CY70Brbb1dGdooLEhh7b0=
|
||||||
|
|
|
@ -55,6 +55,7 @@ type ActionRun struct {
|
||||||
PreviousDuration time.Duration
|
PreviousDuration time.Duration
|
||||||
Created timeutil.TimeStamp `xorm:"created"`
|
Created timeutil.TimeStamp `xorm:"created"`
|
||||||
Updated timeutil.TimeStamp `xorm:"updated"`
|
Updated timeutil.TimeStamp `xorm:"updated"`
|
||||||
|
NotifyEmail bool
|
||||||
}
|
}
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
|
|
|
@ -442,6 +442,12 @@ func (a *Action) GetIssueContent(ctx context.Context) string {
|
||||||
return a.Issue.Content
|
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
|
// GetFeedsOptions options for retrieving feeds
|
||||||
type GetFeedsOptions struct {
|
type GetFeedsOptions struct {
|
||||||
db.ListOptions
|
db.ListOptions
|
||||||
|
@ -595,13 +601,14 @@ func DeleteOldActions(ctx context.Context, olderThan time.Duration) (err error)
|
||||||
}
|
}
|
||||||
|
|
||||||
// NotifyWatchers creates batch of actions for every watcher.
|
// NotifyWatchers creates batch of actions for every watcher.
|
||||||
func NotifyWatchers(ctx context.Context, actions ...*Action) error {
|
func NotifyWatchers(ctx context.Context, actions ...*Action) ([]Action, error) {
|
||||||
var watchers []*repo_model.Watch
|
var watchers []*repo_model.Watch
|
||||||
var repo *repo_model.Repository
|
var repo *repo_model.Repository
|
||||||
var err error
|
var err error
|
||||||
var permCode []bool
|
var permCode []bool
|
||||||
var permIssue []bool
|
var permIssue []bool
|
||||||
var permPR []bool
|
var permPR []bool
|
||||||
|
var out []Action
|
||||||
|
|
||||||
e := db.GetEngine(ctx)
|
e := db.GetEngine(ctx)
|
||||||
|
|
||||||
|
@ -612,14 +619,14 @@ func NotifyWatchers(ctx context.Context, actions ...*Action) error {
|
||||||
// Add feeds for user self and all watchers.
|
// Add feeds for user self and all watchers.
|
||||||
watchers, err = repo_model.GetWatchers(ctx, act.RepoID)
|
watchers, err = repo_model.GetWatchers(ctx, act.RepoID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("get watchers: %w", err)
|
return nil, fmt.Errorf("get watchers: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Be aware that optimizing this correctly into the `GetWatchers` SQL
|
// Be aware that optimizing this correctly into the `GetWatchers` SQL
|
||||||
// query is for most cases less performant than doing this.
|
// query is for most cases less performant than doing this.
|
||||||
blockedDoerUserIDs, err := user_model.ListBlockedByUsersID(ctx, act.ActUserID)
|
blockedDoerUserIDs, err := user_model.ListBlockedByUsersID(ctx, act.ActUserID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("user_model.ListBlockedByUsersID: %w", err)
|
return nil, fmt.Errorf("user_model.ListBlockedByUsersID: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(blockedDoerUserIDs) > 0 {
|
if len(blockedDoerUserIDs) > 0 {
|
||||||
|
@ -634,8 +641,9 @@ func NotifyWatchers(ctx context.Context, actions ...*Action) error {
|
||||||
// Add feed for actioner.
|
// Add feed for actioner.
|
||||||
act.UserID = act.ActUserID
|
act.UserID = act.ActUserID
|
||||||
if _, err = e.Insert(act); err != nil {
|
if _, err = e.Insert(act); err != nil {
|
||||||
return fmt.Errorf("insert new actioner: %w", err)
|
return nil, fmt.Errorf("insert new actioner: %w", err)
|
||||||
}
|
}
|
||||||
|
out = append(out, *act)
|
||||||
|
|
||||||
if repoChanged {
|
if repoChanged {
|
||||||
act.loadRepo(ctx)
|
act.loadRepo(ctx)
|
||||||
|
@ -643,7 +651,7 @@ func NotifyWatchers(ctx context.Context, actions ...*Action) error {
|
||||||
|
|
||||||
// check repo owner exist.
|
// check repo owner exist.
|
||||||
if err := act.Repo.LoadOwner(ctx); err != nil {
|
if err := act.Repo.LoadOwner(ctx); err != nil {
|
||||||
return fmt.Errorf("can't get repo owner: %w", err)
|
return nil, fmt.Errorf("can't get repo owner: %w", err)
|
||||||
}
|
}
|
||||||
} else if act.Repo == nil {
|
} else if act.Repo == nil {
|
||||||
act.Repo = repo
|
act.Repo = repo
|
||||||
|
@ -654,7 +662,7 @@ func NotifyWatchers(ctx context.Context, actions ...*Action) error {
|
||||||
act.ID = 0
|
act.ID = 0
|
||||||
act.UserID = act.Repo.Owner.ID
|
act.UserID = act.Repo.Owner.ID
|
||||||
if err = db.Insert(ctx, act); err != nil {
|
if err = db.Insert(ctx, act); err != nil {
|
||||||
return fmt.Errorf("insert new actioner: %w", err)
|
return nil, fmt.Errorf("insert new actioner: %w", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -707,26 +715,29 @@ func NotifyWatchers(ctx context.Context, actions ...*Action) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
if err = db.Insert(ctx, act); err != nil {
|
if err = db.Insert(ctx, act); err != nil {
|
||||||
return fmt.Errorf("insert new action: %w", err)
|
return nil, fmt.Errorf("insert new action: %w", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return nil
|
return out, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// NotifyWatchersActions creates batch of actions for every watcher.
|
// NotifyWatchersActions creates batch of actions for every watcher.
|
||||||
func NotifyWatchersActions(ctx context.Context, acts []*Action) error {
|
func NotifyWatchersActions(ctx context.Context, acts []*Action) ([]Action, error) {
|
||||||
ctx, committer, err := db.TxContext(ctx)
|
ctx, committer, err := db.TxContext(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return nil, err
|
||||||
}
|
}
|
||||||
defer committer.Close()
|
defer committer.Close()
|
||||||
|
var out []Action
|
||||||
for _, act := range acts {
|
for _, act := range acts {
|
||||||
if err := NotifyWatchers(ctx, act); err != nil {
|
as, err := NotifyWatchers(ctx, act)
|
||||||
return err
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
}
|
}
|
||||||
|
out = append(out, as...)
|
||||||
}
|
}
|
||||||
return committer.Commit()
|
return out, committer.Commit()
|
||||||
}
|
}
|
||||||
|
|
||||||
// DeleteIssueActions delete all actions related with issueID
|
// DeleteIssueActions delete all actions related with issueID
|
||||||
|
|
|
@ -197,7 +197,8 @@ func TestNotifyWatchers(t *testing.T) {
|
||||||
RepoID: 1,
|
RepoID: 1,
|
||||||
OpType: activities_model.ActionStarRepo,
|
OpType: activities_model.ActionStarRepo,
|
||||||
}
|
}
|
||||||
require.NoError(t, activities_model.NotifyWatchers(db.DefaultContext, action))
|
_, err := activities_model.NotifyWatchers(db.DefaultContext, action)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
// One watchers are inactive, thus action is only created for user 8, 1, 4, 11
|
// One watchers are inactive, thus action is only created for user 8, 1, 4, 11
|
||||||
unittest.AssertExistsAndLoadBean(t, &activities_model.Action{
|
unittest.AssertExistsAndLoadBean(t, &activities_model.Action{
|
||||||
|
|
106
models/activities/federated_user_activity.go
Normal file
106
models/activities/federated_user_activity.go
Normal file
|
@ -0,0 +1,106 @@
|
||||||
|
// 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
|
||||||
|
}
|
24
models/activities/federated_user_activity_test.go
Normal file
24
models/activities/federated_user_activity_test.go
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
// 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())
|
||||||
|
}
|
||||||
|
}
|
|
@ -153,3 +153,304 @@
|
||||||
issue_id: 19 # in repo_id 58
|
issue_id: 19 # in repo_id 58
|
||||||
content: '{"is_force_push":true,"commit_ids":["1978192d98bb1b65e11c2cf37da854fbf94bffd6", "9b93963cf6de4dc33f915bb67f192d099c301f43"]}'
|
content: '{"is_force_push":true,"commit_ids":["1978192d98bb1b65e11c2cf37da854fbf94bffd6", "9b93963cf6de4dc33f915bb67f192d099c301f43"]}'
|
||||||
created_unix: 1749734240
|
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
|
||||||
|
|
|
@ -6,6 +6,7 @@ package forgefed
|
||||||
import (
|
import (
|
||||||
"database/sql"
|
"database/sql"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"net/url"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
@ -17,9 +18,9 @@ import (
|
||||||
// swagger:model
|
// swagger:model
|
||||||
type FederationHost struct {
|
type FederationHost struct {
|
||||||
ID int64 `xorm:"pk autoincr"`
|
ID int64 `xorm:"pk autoincr"`
|
||||||
HostFqdn string `xorm:"host_fqdn UNIQUE INDEX VARCHAR(255) NOT NULL"`
|
HostFqdn string `xorm:"host_fqdn UNIQUE(federation_host) INDEX VARCHAR(255) NOT NULL"`
|
||||||
|
HostPort uint16 `xorm:" UNIQUE(federation_host) INDEX NOT NULL DEFAULT 443"`
|
||||||
NodeInfo NodeInfo `xorm:"extends NOT NULL"`
|
NodeInfo NodeInfo `xorm:"extends NOT NULL"`
|
||||||
HostPort uint16 `xorm:"NOT NULL DEFAULT 443"`
|
|
||||||
HostSchema string `xorm:"NOT NULL DEFAULT 'https'"`
|
HostSchema string `xorm:"NOT NULL DEFAULT 'https'"`
|
||||||
LatestActivity time.Time `xorm:"NOT NULL"`
|
LatestActivity time.Time `xorm:"NOT NULL"`
|
||||||
KeyID sql.NullString `xorm:"key_id UNIQUE"`
|
KeyID sql.NullString `xorm:"key_id UNIQUE"`
|
||||||
|
@ -42,6 +43,13 @@ func NewFederationHost(hostFqdn string, nodeInfo NodeInfo, port uint16, schema s
|
||||||
return result, nil
|
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
|
// Validate collects error strings in a slice and returns this
|
||||||
func (host FederationHost) Validate() []string {
|
func (host FederationHost) Validate() []string {
|
||||||
var result []string
|
var result []string
|
||||||
|
|
|
@ -19,10 +19,12 @@ type (
|
||||||
const (
|
const (
|
||||||
ForgejoSourceType SoftwareNameType = "forgejo"
|
ForgejoSourceType SoftwareNameType = "forgejo"
|
||||||
GiteaSourceType SoftwareNameType = "gitea"
|
GiteaSourceType SoftwareNameType = "gitea"
|
||||||
|
MastodonSourceType SoftwareNameType = "mastodon"
|
||||||
|
GoToSocialSourceType SoftwareNameType = "gotosocial"
|
||||||
)
|
)
|
||||||
|
|
||||||
var KnownSourceTypes = []any{
|
var KnownSourceTypes = []any{
|
||||||
ForgejoSourceType, GiteaSourceType,
|
ForgejoSourceType, GiteaSourceType, MastodonSourceType, GoToSocialSourceType,
|
||||||
}
|
}
|
||||||
|
|
||||||
// ------------------------------------------------ NodeInfoWellKnown ------------------------------------------------
|
// ------------------------------------------------ NodeInfoWellKnown ------------------------------------------------
|
||||||
|
|
|
@ -103,6 +103,12 @@ var migrations = []*Migration{
|
||||||
NewMigration("Normalize repository.topics to empty slice instead of null", SetTopicsAsEmptySlice),
|
NewMigration("Normalize repository.topics to empty slice instead of null", SetTopicsAsEmptySlice),
|
||||||
// v31 -> v32
|
// v31 -> v32
|
||||||
NewMigration("Migrate maven package name concatenation", ChangeMavenArtifactConcatenation),
|
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.
|
// GetCurrentDBVersion returns the current Forgejo database version.
|
||||||
|
|
126
models/forgejo_migrations/v33.go
Normal file
126
models/forgejo_migrations/v33.go
Normal file
|
@ -0,0 +1,126 @@
|
||||||
|
// 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
|
||||||
|
}
|
46
models/forgejo_migrations/v33_test.go
Normal file
46
models/forgejo_migrations/v33_test.go
Normal file
|
@ -0,0 +1,46 @@
|
||||||
|
// 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)
|
||||||
|
}
|
14
models/forgejo_migrations/v34.go
Normal file
14
models/forgejo_migrations/v34.go
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
// 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))
|
||||||
|
}
|
19
models/forgejo_migrations/v35.go
Normal file
19
models/forgejo_migrations/v35.go
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
// 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{})
|
||||||
|
}
|
|
@ -5,6 +5,7 @@
|
||||||
package issues
|
package issues
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bufio"
|
||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
@ -923,31 +924,30 @@ func MergeBlockedByOutdatedBranch(protectBranch *git_model.ProtectedBranch, pr *
|
||||||
return protectBranch.BlockOnOutdatedBranch && pr.CommitsBehind > 0
|
return protectBranch.BlockOnOutdatedBranch && pr.CommitsBehind > 0
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetCodeOwnersFromContent returns the code owners configuration
|
// GetCodeOwnersFromReader returns the code owners configuration
|
||||||
// Return empty slice if files missing
|
|
||||||
// Return warning messages on parsing errors
|
// Return warning messages on parsing errors
|
||||||
// We're trying to do the best we can when parsing a file.
|
// We're trying to do the best we can when parsing a file.
|
||||||
// Invalid lines are skipped. Non-existent users and teams too.
|
// Invalid lines are skipped. Non-existent users and teams too.
|
||||||
func GetCodeOwnersFromContent(ctx context.Context, data string) ([]*CodeOwnerRule, []string) {
|
func GetCodeOwnersFromReader(ctx context.Context, rc io.ReadCloser, truncated bool) ([]*CodeOwnerRule, []string) {
|
||||||
if len(data) == 0 {
|
defer rc.Close()
|
||||||
return nil, nil
|
scanner := bufio.NewScanner(rc)
|
||||||
}
|
|
||||||
|
|
||||||
rules := make([]*CodeOwnerRule, 0)
|
var rules []*CodeOwnerRule
|
||||||
lines := strings.Split(data, "\n")
|
var warnings []string
|
||||||
warnings := make([]string, 0)
|
line := 0
|
||||||
|
for scanner.Scan() {
|
||||||
|
line++
|
||||||
|
|
||||||
for i, line := range lines {
|
tokens := TokenizeCodeOwnersLine(scanner.Text())
|
||||||
tokens := TokenizeCodeOwnersLine(line)
|
|
||||||
if len(tokens) == 0 {
|
if len(tokens) == 0 {
|
||||||
continue
|
continue
|
||||||
} else if len(tokens) < 2 {
|
} else if len(tokens) < 2 {
|
||||||
warnings = append(warnings, fmt.Sprintf("Line: %d: incorrect format", i+1))
|
warnings = append(warnings, fmt.Sprintf("Line: %d: incorrect format", line))
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
rule, wr := ParseCodeOwnersLine(ctx, tokens)
|
rule, wr := ParseCodeOwnersLine(ctx, tokens)
|
||||||
for _, w := range wr {
|
for _, w := range wr {
|
||||||
warnings = append(warnings, fmt.Sprintf("Line: %d: %s", i+1, w))
|
warnings = append(warnings, fmt.Sprintf("Line: %d: %s", line, w))
|
||||||
}
|
}
|
||||||
if rule == nil {
|
if rule == nil {
|
||||||
continue
|
continue
|
||||||
|
@ -955,6 +955,12 @@ func GetCodeOwnersFromContent(ctx context.Context, data string) ([]*CodeOwnerRul
|
||||||
|
|
||||||
rules = append(rules, rule)
|
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
|
return rules, warnings
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,19 +11,21 @@ import (
|
||||||
|
|
||||||
type FederatedUser struct {
|
type FederatedUser struct {
|
||||||
ID int64 `xorm:"pk autoincr"`
|
ID int64 `xorm:"pk autoincr"`
|
||||||
UserID int64 `xorm:"NOT NULL"`
|
UserID int64 `xorm:"NOT NULL INDEX user_id"`
|
||||||
ExternalID string `xorm:"UNIQUE(federation_user_mapping) NOT NULL"`
|
ExternalID string `xorm:"UNIQUE(federation_user_mapping) NOT NULL"`
|
||||||
FederationHostID int64 `xorm:"UNIQUE(federation_user_mapping) NOT NULL"`
|
FederationHostID int64 `xorm:"UNIQUE(federation_user_mapping) NOT NULL"`
|
||||||
KeyID sql.NullString `xorm:"key_id UNIQUE"`
|
KeyID sql.NullString `xorm:"key_id UNIQUE"`
|
||||||
PublicKey sql.Null[sql.RawBytes] `xorm:"BLOB"`
|
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, normalizedOriginalURL string) (FederatedUser, error) {
|
func NewFederatedUser(userID int64, externalID string, federationHostID int64, inboxPath, normalizedOriginalURL string) (FederatedUser, error) {
|
||||||
result := FederatedUser{
|
result := FederatedUser{
|
||||||
UserID: userID,
|
UserID: userID,
|
||||||
ExternalID: externalID,
|
ExternalID: externalID,
|
||||||
FederationHostID: federationHostID,
|
FederationHostID: federationHostID,
|
||||||
|
InboxPath: inboxPath,
|
||||||
NormalizedOriginalURL: normalizedOriginalURL,
|
NormalizedOriginalURL: normalizedOriginalURL,
|
||||||
}
|
}
|
||||||
if valid, err := validation.IsValid(result); !valid {
|
if valid, err := validation.IsValid(result); !valid {
|
||||||
|
@ -32,10 +34,11 @@ func NewFederatedUser(userID int64, externalID string, federationHostID int64, n
|
||||||
return result, nil
|
return result, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (user FederatedUser) Validate() []string {
|
func (federatedUser FederatedUser) Validate() []string {
|
||||||
var result []string
|
var result []string
|
||||||
result = append(result, validation.ValidateNotEmpty(user.UserID, "UserID")...)
|
result = append(result, validation.ValidateNotEmpty(federatedUser.UserID, "UserID")...)
|
||||||
result = append(result, validation.ValidateNotEmpty(user.ExternalID, "ExternalID")...)
|
result = append(result, validation.ValidateNotEmpty(federatedUser.ExternalID, "ExternalID")...)
|
||||||
result = append(result, validation.ValidateNotEmpty(user.FederationHostID, "FederationHostID")...)
|
result = append(result, validation.ValidateNotEmpty(federatedUser.FederationHostID, "FederationHostID")...)
|
||||||
|
result = append(result, validation.ValidateNotEmpty(federatedUser.InboxPath, "InboxPath")...)
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
30
models/user/federated_user_follower.go
Normal file
30
models/user/federated_user_follower.go
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
// 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
|
||||||
|
}
|
27
models/user/federated_user_follower_test.go
Normal file
27
models/user/federated_user_follower_test.go
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
// 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")
|
||||||
|
}
|
|
@ -14,6 +14,7 @@ func Test_FederatedUserValidation(t *testing.T) {
|
||||||
UserID: 12,
|
UserID: 12,
|
||||||
ExternalID: "12",
|
ExternalID: "12",
|
||||||
FederationHostID: 1,
|
FederationHostID: 1,
|
||||||
|
InboxPath: "/api/v1/activitypub/user-id/12/inbox",
|
||||||
}
|
}
|
||||||
if res, err := validation.IsValid(sut); !res {
|
if res, err := validation.IsValid(sut); !res {
|
||||||
t.Errorf("sut should be valid but was %q", err)
|
t.Errorf("sut should be valid but was %q", err)
|
||||||
|
@ -22,6 +23,7 @@ func Test_FederatedUserValidation(t *testing.T) {
|
||||||
sut = FederatedUser{
|
sut = FederatedUser{
|
||||||
ExternalID: "12",
|
ExternalID: "12",
|
||||||
FederationHostID: 1,
|
FederationHostID: 1,
|
||||||
|
InboxPath: "/api/v1/activitypub/user-id/12/inbox",
|
||||||
}
|
}
|
||||||
if res, _ := validation.IsValid(sut); res {
|
if res, _ := validation.IsValid(sut); res {
|
||||||
t.Error("sut should be invalid")
|
t.Error("sut should be invalid")
|
||||||
|
|
|
@ -11,6 +11,7 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
// Follow represents relations of user and their followers.
|
// 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 {
|
type Follow struct {
|
||||||
ID int64 `xorm:"pk autoincr"`
|
ID int64 `xorm:"pk autoincr"`
|
||||||
UserID int64 `xorm:"UNIQUE(follow)"`
|
UserID int64 `xorm:"UNIQUE(follow)"`
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
// Copyright 2024 The Forgejo Authors. All rights reserved.
|
// Copyright 2024, 2025 The Forgejo Authors. All rights reserved.
|
||||||
// SPDX-License-Identifier: MIT
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
package user
|
package user
|
||||||
|
@ -8,12 +8,14 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
"forgejo.org/models/db"
|
"forgejo.org/models/db"
|
||||||
|
"forgejo.org/modules/log"
|
||||||
"forgejo.org/modules/optional"
|
"forgejo.org/modules/optional"
|
||||||
"forgejo.org/modules/validation"
|
"forgejo.org/modules/validation"
|
||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
db.RegisterModel(new(FederatedUser))
|
db.RegisterModel(new(FederatedUser))
|
||||||
|
db.RegisterModel(new(FederatedUserFollower))
|
||||||
}
|
}
|
||||||
|
|
||||||
func CreateFederatedUser(ctx context.Context, user *User, federatedUser *FederatedUser) error {
|
func CreateFederatedUser(ctx context.Context, user *User, federatedUser *FederatedUser) error {
|
||||||
|
@ -30,7 +32,12 @@ func CreateFederatedUser(ctx context.Context, user *User, federatedUser *Federat
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
defer committer.Close()
|
defer func() {
|
||||||
|
err := committer.Close()
|
||||||
|
if err != nil {
|
||||||
|
log.Error("Error closing committer: %v", err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
if err := CreateUser(ctx, user, &overwrite); err != nil {
|
if err := CreateUser(ctx, user, &overwrite); err != nil {
|
||||||
return err
|
return err
|
||||||
|
@ -50,6 +57,14 @@ func CreateFederatedUser(ctx context.Context, user *User, federatedUser *Federat
|
||||||
return committer.Commit()
|
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) {
|
func FindFederatedUser(ctx context.Context, externalID string, federationHostID int64) (*User, *FederatedUser, error) {
|
||||||
federatedUser := new(FederatedUser)
|
federatedUser := new(FederatedUser)
|
||||||
user := new(User)
|
user := new(User)
|
||||||
|
@ -75,6 +90,41 @@ func FindFederatedUser(ctx context.Context, externalID string, federationHostID
|
||||||
return user, federatedUser, nil
|
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) {
|
func FindFederatedUserByKeyID(ctx context.Context, keyID string) (*User, *FederatedUser, error) {
|
||||||
federatedUser := new(FederatedUser)
|
federatedUser := new(FederatedUser)
|
||||||
user := new(User)
|
user := new(User)
|
||||||
|
@ -101,7 +151,85 @@ func FindFederatedUserByKeyID(ctx context.Context, keyID string) (*User, *Federa
|
||||||
return user, federatedUser, nil
|
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 {
|
func DeleteFederatedUser(ctx context.Context, userID int64) error {
|
||||||
_, err := db.GetEngine(ctx).Delete(&FederatedUser{UserID: userID})
|
_, err := db.GetEngine(ctx).Delete(&FederatedUser{UserID: userID})
|
||||||
return err
|
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,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
|
@ -12,6 +12,13 @@ import (
|
||||||
"forgejo.org/modules/structs"
|
"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 (
|
const (
|
||||||
GhostUserID = -1
|
GhostUserID = -1
|
||||||
GhostUserName = "Ghost"
|
GhostUserName = "Ghost"
|
||||||
|
|
|
@ -148,7 +148,7 @@ func TestAPActorID_APActorID(t *testing.T) {
|
||||||
assert.Equal(t, expected, url)
|
assert.Equal(t, expected, url)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestAPActorKeyID(t *testing.T) {
|
func TestKeyID(t *testing.T) {
|
||||||
user := user_model.User{ID: 1}
|
user := user_model.User{ID: 1}
|
||||||
url := user.APActorKeyID()
|
url := user.APActorKeyID()
|
||||||
expected := "https://try.gitea.io/api/v1/activitypub/user-id/1#main-key"
|
expected := "https://try.gitea.io/api/v1/activitypub/user-id/1#main-key"
|
||||||
|
|
|
@ -12,7 +12,6 @@ import (
|
||||||
|
|
||||||
"forgejo.org/modules/log"
|
"forgejo.org/modules/log"
|
||||||
"forgejo.org/modules/typesniffer"
|
"forgejo.org/modules/typesniffer"
|
||||||
"forgejo.org/modules/util"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// Blob represents a Git object.
|
// Blob represents a Git object.
|
||||||
|
@ -25,42 +24,25 @@ type Blob struct {
|
||||||
repo *Repository
|
repo *Repository
|
||||||
}
|
}
|
||||||
|
|
||||||
// DataAsync gets a ReadCloser for the contents of a blob without reading it all.
|
func (b *Blob) newReader() (*bufio.Reader, int64, func(), error) {
|
||||||
// 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)
|
wr, rd, cancel, err := b.repo.CatFileBatch(b.repo.Ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, 0, nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
_, err = wr.Write([]byte(b.ID.String() + "\n"))
|
_, err = wr.Write([]byte(b.ID.String() + "\n"))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
cancel()
|
cancel()
|
||||||
return nil, err
|
return nil, 0, nil, err
|
||||||
}
|
}
|
||||||
_, _, size, err := ReadBatchLine(rd)
|
_, _, size, err := ReadBatchLine(rd)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
cancel()
|
cancel()
|
||||||
return nil, err
|
return nil, 0, nil, err
|
||||||
}
|
}
|
||||||
b.gotSize = true
|
b.gotSize = true
|
||||||
b.size = size
|
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
|
// Size returns the uncompressed size of the blob
|
||||||
|
@ -91,9 +73,35 @@ func (b *Blob) Size() int64 {
|
||||||
return b.size
|
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 {
|
type blobReader struct {
|
||||||
rd *bufio.Reader
|
rd *bufio.Reader
|
||||||
n int64
|
n int64 // number of bytes to read
|
||||||
|
additionalDiscard int64 // additional number of bytes to discard
|
||||||
cancel func()
|
cancel func()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -117,7 +125,8 @@ func (b *blobReader) Close() error {
|
||||||
|
|
||||||
defer b.cancel()
|
defer b.cancel()
|
||||||
|
|
||||||
if err := DiscardFull(b.rd, b.n+1); err != nil {
|
// discard the unread bytes, the truncated bytes and the trailing newline
|
||||||
|
if err := DiscardFull(b.rd, b.n+b.additionalDiscard+1); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -131,17 +140,35 @@ func (b *Blob) Name() string {
|
||||||
return b.name
|
return b.name
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetBlobContent Gets the limited content of the blob as raw text
|
// 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
|
||||||
func (b *Blob) GetBlobContent(limit int64) (string, error) {
|
func (b *Blob) GetBlobContent(limit int64) (string, error) {
|
||||||
if limit <= 0 {
|
if limit <= 0 {
|
||||||
return "", nil
|
return "", nil
|
||||||
}
|
}
|
||||||
dataRc, err := b.DataAsync()
|
rc, fullSize, err := b.NewTruncatedReader(limit)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
defer dataRc.Close()
|
defer rc.Close()
|
||||||
buf, err := util.ReadWithLimit(dataRc, int(limit))
|
|
||||||
|
buf := make([]byte, min(fullSize, limit))
|
||||||
|
_, err = io.ReadFull(rc, buf)
|
||||||
return string(buf), err
|
return string(buf), err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -35,6 +35,106 @@ func TestBlob_Data(t *testing.T) {
|
||||||
assert.Equal(t, output, string(data))
|
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) {
|
func Benchmark_Blob_Data(b *testing.B) {
|
||||||
bareRepo1Path := filepath.Join(testReposDir, "repo1_bare")
|
bareRepo1Path := filepath.Join(testReposDir, "repo1_bare")
|
||||||
repo, err := openRepositoryWithDefaultContext(bareRepo1Path)
|
repo, err := openRepositoryWithDefaultContext(bareRepo1Path)
|
||||||
|
|
|
@ -267,8 +267,13 @@ 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
|
// RenderString renders Markdown string to HTML with all specific handling stuff and return string
|
||||||
func RenderString(ctx *markup.RenderContext, content string) (template.HTML, error) {
|
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
|
var buf strings.Builder
|
||||||
if err := Render(ctx, strings.NewReader(content), &buf); err != nil {
|
if err := Render(ctx, input, &buf); err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
return template.HTML(buf.String()), nil
|
return template.HTML(buf.String()), nil
|
||||||
|
|
|
@ -23,6 +23,11 @@ var wellKnownMimeTypesLower = map[string]string{
|
||||||
".wasm": "application/wasm",
|
".wasm": "application/wasm",
|
||||||
".webp": "image/webp",
|
".webp": "image/webp",
|
||||||
".xml": "text/xml; charset=utf-8",
|
".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
|
// well, there are some types missing from the builtin list
|
||||||
".txt": "text/plain; charset=utf-8",
|
".txt": "text/plain; charset=utf-8",
|
||||||
|
|
|
@ -78,3 +78,9 @@ type ActionRun struct {
|
||||||
// the url of this action run
|
// the url of this action run
|
||||||
HTMLURL string `json:"html_url"`
|
HTMLURL string `json:"html_url"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ListActionRunResponse return a list of ActionRun
|
||||||
|
type ListActionRunResponse struct {
|
||||||
|
Entries []*ActionRun `json:"workflow_runs"`
|
||||||
|
TotalCount int64 `json:"total_count"`
|
||||||
|
}
|
||||||
|
|
|
@ -32,23 +32,3 @@ type ActionTaskResponse struct {
|
||||||
Entries []*ActionTask `json:"workflow_runs"`
|
Entries []*ActionTask `json:"workflow_runs"`
|
||||||
TotalCount int64 `json:"total_count"`
|
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"`
|
|
||||||
}
|
|
||||||
|
|
|
@ -24,6 +24,16 @@ const (
|
||||||
AvifMimeType = "image/avif"
|
AvifMimeType = "image/avif"
|
||||||
// ApplicationOctetStream MIME type of binary files.
|
// ApplicationOctetStream MIME type of binary files.
|
||||||
ApplicationOctetStream = "application/octet-stream"
|
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 (
|
var (
|
||||||
|
@ -67,6 +77,36 @@ func (ct SniffedType) IsAudio() bool {
|
||||||
return strings.Contains(ct.contentType, "audio/")
|
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
|
// IsRepresentableAsText returns true if file content can be represented as
|
||||||
// plain text or is empty.
|
// plain text or is empty.
|
||||||
func (ct SniffedType) IsRepresentableAsText() bool {
|
func (ct SniffedType) IsRepresentableAsText() bool {
|
||||||
|
@ -75,7 +115,7 @@ func (ct SniffedType) IsRepresentableAsText() bool {
|
||||||
|
|
||||||
// IsBrowsableBinaryType returns whether a non-text type can be displayed in a browser
|
// IsBrowsableBinaryType returns whether a non-text type can be displayed in a browser
|
||||||
func (ct SniffedType) IsBrowsableBinaryType() bool {
|
func (ct SniffedType) IsBrowsableBinaryType() bool {
|
||||||
return ct.IsImage() || ct.IsSvgImage() || ct.IsPDF() || ct.IsVideo() || ct.IsAudio()
|
return ct.IsImage() || ct.IsSvgImage() || ct.IsPDF() || ct.IsVideo() || ct.IsAudio() || ct.Is3DModel()
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetMimeType returns the mime type
|
// GetMimeType returns the mime type
|
||||||
|
@ -135,6 +175,13 @@ func DetectContentType(data []byte) SniffedType {
|
||||||
ct = "audio/ogg" // for most cases, it is used as an audio container
|
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}
|
return SniffedType{ct}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -117,6 +117,14 @@ func TestIsAudio(t *testing.T) {
|
||||||
assert.True(t, DetectContentType([]byte("ID3Toy\n====\t* hi 🌞, ..."+"🌛"[0:2])).IsText()) // test ID3 tag with incomplete UTF8 char
|
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) {
|
func TestDetectContentTypeFromReader(t *testing.T) {
|
||||||
mp3, _ := base64.StdEncoding.DecodeString("SUQzBAAAAAABAFRYWFgAAAASAAADbWFqb3JfYnJhbmQAbXA0MgBUWFhYAAAAEQAAA21pbm9yX3Zl")
|
mp3, _ := base64.StdEncoding.DecodeString("SUQzBAAAAAABAFRYWFgAAAASAAADbWFqb3JfYnJhbmQAbXA0MgBUWFhYAAAAEQAAA21pbm9yX3Zl")
|
||||||
st, err := DetectContentTypeFromReader(bytes.NewReader(mp3))
|
st, err := DetectContentTypeFromReader(bytes.NewReader(mp3))
|
||||||
|
@ -145,3 +153,15 @@ func TestDetectContentTypeAvif(t *testing.T) {
|
||||||
|
|
||||||
assert.True(t, st.IsImage())
|
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())
|
||||||
|
}
|
||||||
|
|
|
@ -4,7 +4,6 @@
|
||||||
package util
|
package util
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
|
||||||
"errors"
|
"errors"
|
||||||
"io"
|
"io"
|
||||||
)
|
)
|
||||||
|
@ -20,42 +19,6 @@ func ReadAtMost(r io.Reader, buf []byte) (n int, err error) {
|
||||||
return n, err
|
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
|
// ErrNotEmpty is an error reported when there is a non-empty reader
|
||||||
var ErrNotEmpty = errors.New("not-empty")
|
var ErrNotEmpty = errors.New("not-empty")
|
||||||
|
|
||||||
|
|
|
@ -1,67 +0,0 @@
|
||||||
// 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)
|
|
||||||
}
|
|
|
@ -768,8 +768,8 @@ update_profile_success = Your profile has been updated.
|
||||||
change_username = Your username has been changed.
|
change_username = Your username has been changed.
|
||||||
change_username_prompt = Note: Changing your username also changes your account URL.
|
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 = 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.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.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
|
continue = Continue
|
||||||
cancel = Cancel
|
cancel = Cancel
|
||||||
language = Language
|
language = Language
|
||||||
|
@ -1694,15 +1694,13 @@ issues.close_comment_issue = Close with comment
|
||||||
issues.reopen_issue = Reopen
|
issues.reopen_issue = Reopen
|
||||||
issues.reopen_comment_issue = Reopen with comment
|
issues.reopen_comment_issue = Reopen with comment
|
||||||
issues.create_comment = Comment
|
issues.create_comment = Comment
|
||||||
issues.closed_at = `closed this issue <a id="%[1]s" href="#%[1]s">%[2]s</a>`
|
issues.closed_at = `closed this issue %s`
|
||||||
issues.reopened_at = `reopened this issue <a id="%[1]s" href="#%[1]s">%[2]s</a>`
|
issues.reopened_at = `reopened this issue %s`
|
||||||
issues.commit_ref_at = `referenced this issue from a commit <a id="%[1]s" href="#%[1]s">%[2]s</a>`
|
issues.commit_ref_at = `referenced this issue from a commit %s`
|
||||||
issues.ref_issue_from = `<a href="%[3]s">referenced this issue %[4]s</a> <a id="%[1]s" href="#%[1]s">%[2]s</a>`
|
issues.ref_issue_from = `<a href="%[2]s">referenced this issue %[3]s</a> %[1]s`
|
||||||
issues.ref_pull_from = `<a href="%[3]s">referenced this pull request %[4]s</a> <a id="%[1]s" href="#%[1]s">%[2]s</a>`
|
issues.ref_pull_from = `<a href="%[2]s">referenced this pull request %[3]s</a> %[1]s`
|
||||||
issues.ref_closing_from = `<a href="%[3]s">referenced this issue from a pull request %[4]s that will close it</a>, <a id="%[1]s" href="#%[1]s">%[2]s</a>`
|
issues.ref_closing_from = `<a href="%[2]s">referenced this issue from a pull request %[3]s that will close it</a>, %[1]s`
|
||||||
issues.ref_reopening_from = `<a href="%[3]s">referenced this issue from a pull request %[4]s that will reopen it</a>, <a id="%[1]s" href="#%[1]s">%[2]s</a>`
|
issues.ref_reopening_from = `<a href="%[2]s">referenced this issue from a pull request %[3]s that will reopen it</a>, %[1]s`
|
||||||
issues.ref_closed_from = `<a href="%[3]s">closed this issue %[4]s</a> <a id="%[1]s" href="#%[1]s">%[2]s</a>`
|
|
||||||
issues.ref_reopened_from = `<a href="%[3]s">reopened this issue %[4]s</a> <a id="%[1]s" href="#%[1]s">%[2]s</a>`
|
|
||||||
issues.ref_from = `from %[1]s`
|
issues.ref_from = `from %[1]s`
|
||||||
issues.author = Author
|
issues.author = Author
|
||||||
issues.author.tooltip.issue = This user is the author of this issue.
|
issues.author.tooltip.issue = This user is the author of this issue.
|
||||||
|
@ -2014,9 +2012,9 @@ pulls.update_branch_success = Branch update was successful
|
||||||
pulls.update_not_allowed = You are not allowed to update branch
|
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.outdated_with_base_branch = This branch is out-of-date with the base branch
|
||||||
pulls.close = Close pull request
|
pulls.close = Close pull request
|
||||||
pulls.closed_at = `closed this pull request <a id="%[1]s" href="#%[1]s">%[2]s</a>`
|
pulls.closed_at = `closed this pull request %s`
|
||||||
pulls.reopened_at = `reopened this pull request <a id="%[1]s" href="#%[1]s">%[2]s</a>`
|
pulls.reopened_at = `reopened this pull request %s`
|
||||||
pulls.commit_ref_at = `referenced this pull request from a commit <a id="%[1]s" href="#%[1]s">%[2]s</a>`
|
pulls.commit_ref_at = `referenced this pull request from a commit %s`
|
||||||
pulls.cmd_instruction_hint = View command line instructions
|
pulls.cmd_instruction_hint = View command line instructions
|
||||||
pulls.cmd_instruction_checkout_title = Checkout
|
pulls.cmd_instruction_checkout_title = Checkout
|
||||||
pulls.cmd_instruction_checkout_desc = From your project repository, check out a new branch and test the changes.
|
pulls.cmd_instruction_checkout_desc = From your project repository, check out a new branch and test the changes.
|
||||||
|
@ -2933,8 +2931,8 @@ settings.update_settings = Update settings
|
||||||
settings.update_setting_success = Organization settings have been updated.
|
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_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 = 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.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.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.update_avatar_success = The organization's avatar has been updated.
|
||||||
settings.delete = Delete organization
|
settings.delete = Delete organization
|
||||||
settings.delete_account = Delete this organization
|
settings.delete_account = Delete this organization
|
||||||
|
|
140
package-lock.json
generated
140
package-lock.json
generated
|
@ -12,6 +12,7 @@
|
||||||
"@github/markdown-toolbar-element": "2.2.3",
|
"@github/markdown-toolbar-element": "2.2.3",
|
||||||
"@github/quote-selection": "2.1.0",
|
"@github/quote-selection": "2.1.0",
|
||||||
"@github/text-expander-element": "2.8.0",
|
"@github/text-expander-element": "2.8.0",
|
||||||
|
"@google/model-viewer": "4.1.0",
|
||||||
"@mcaptcha/vanilla-glue": "0.1.0-alpha-3",
|
"@mcaptcha/vanilla-glue": "0.1.0-alpha-3",
|
||||||
"@primer/octicons": "19.14.0",
|
"@primer/octicons": "19.14.0",
|
||||||
"ansi_up": "6.0.5",
|
"ansi_up": "6.0.5",
|
||||||
|
@ -62,7 +63,7 @@
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@axe-core/playwright": "4.10.2",
|
"@axe-core/playwright": "4.10.2",
|
||||||
"@eslint-community/eslint-plugin-eslint-comments": "4.5.0",
|
"@eslint-community/eslint-plugin-eslint-comments": "4.5.0",
|
||||||
"@playwright/test": "1.53.0",
|
"@playwright/test": "1.52.0",
|
||||||
"@stoplight/spectral-cli": "6.15.0",
|
"@stoplight/spectral-cli": "6.15.0",
|
||||||
"@stylistic/eslint-plugin": "4.4.1",
|
"@stylistic/eslint-plugin": "4.4.1",
|
||||||
"@stylistic/stylelint-plugin": "3.1.2",
|
"@stylistic/stylelint-plugin": "3.1.2",
|
||||||
|
@ -1222,6 +1223,22 @@
|
||||||
"dom-input-range": "^1.2.0"
|
"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": {
|
"node_modules/@humanfs/core": {
|
||||||
"version": "0.19.1",
|
"version": "0.19.1",
|
||||||
"resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz",
|
"resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz",
|
||||||
|
@ -2004,6 +2021,21 @@
|
||||||
"integrity": "sha512-M5UknZPHRu3DEDWoipU6sE8PdkZ6Z/S+v4dD+Ke8IaNlpdSQah50lz1KtcFBa2vsdOnwbbnxJwVM4wty6udA5w==",
|
"integrity": "sha512-M5UknZPHRu3DEDWoipU6sE8PdkZ6Z/S+v4dD+Ke8IaNlpdSQah50lz1KtcFBa2vsdOnwbbnxJwVM4wty6udA5w==",
|
||||||
"license": "MIT"
|
"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": {
|
"node_modules/@mcaptcha/core-glue": {
|
||||||
"version": "0.1.0-alpha-5",
|
"version": "0.1.0-alpha-5",
|
||||||
"resolved": "https://registry.npmjs.org/@mcaptcha/core-glue/-/core-glue-0.1.0-alpha-5.tgz",
|
"resolved": "https://registry.npmjs.org/@mcaptcha/core-glue/-/core-glue-0.1.0-alpha-5.tgz",
|
||||||
|
@ -2064,6 +2096,18 @@
|
||||||
"langium": "3.3.1"
|
"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": {
|
"node_modules/@napi-rs/wasm-runtime": {
|
||||||
"version": "0.2.11",
|
"version": "0.2.11",
|
||||||
"resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-0.2.11.tgz",
|
"resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-0.2.11.tgz",
|
||||||
|
@ -2143,13 +2187,13 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@playwright/test": {
|
"node_modules/@playwright/test": {
|
||||||
"version": "1.53.0",
|
"version": "1.52.0",
|
||||||
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.53.0.tgz",
|
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.52.0.tgz",
|
||||||
"integrity": "sha512-15hjKreZDcp7t6TL/7jkAo6Df5STZN09jGiv5dbP9A6vMVncXRqE7/B2SncsyOwrkZRBH2i6/TPOL8BVmm3c7w==",
|
"integrity": "sha512-uh6W7sb55hl7D6vsAeA+V2p5JnlAqzhqFyF0VcJkKZXkgnFcVG9PziERRHQfPLfNGx1C292a4JqbWzhR8L4R1g==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"playwright": "1.53.0"
|
"playwright": "1.52.0"
|
||||||
},
|
},
|
||||||
"bin": {
|
"bin": {
|
||||||
"playwright": "cli.js"
|
"playwright": "cli.js"
|
||||||
|
@ -3493,8 +3537,7 @@
|
||||||
"version": "2.0.7",
|
"version": "2.0.7",
|
||||||
"resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz",
|
"resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz",
|
||||||
"integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==",
|
"integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==",
|
||||||
"license": "MIT",
|
"license": "MIT"
|
||||||
"optional": true
|
|
||||||
},
|
},
|
||||||
"node_modules/@types/unist": {
|
"node_modules/@types/unist": {
|
||||||
"version": "2.0.11",
|
"version": "2.0.11",
|
||||||
|
@ -8768,6 +8811,12 @@
|
||||||
"node": ">= 4"
|
"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": {
|
"node_modules/immer": {
|
||||||
"version": "9.0.21",
|
"version": "9.0.21",
|
||||||
"resolved": "https://registry.npmjs.org/immer/-/immer-9.0.21.tgz",
|
"resolved": "https://registry.npmjs.org/immer/-/immer-9.0.21.tgz",
|
||||||
|
@ -9307,6 +9356,12 @@
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"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": {
|
"node_modules/is-proto-prop": {
|
||||||
"version": "3.0.1",
|
"version": "3.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/is-proto-prop/-/is-proto-prop-3.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/is-proto-prop/-/is-proto-prop-3.0.1.tgz",
|
||||||
|
@ -10023,6 +10078,15 @@
|
||||||
"npm": ">=8"
|
"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": {
|
"node_modules/lilconfig": {
|
||||||
"version": "3.1.3",
|
"version": "3.1.3",
|
||||||
"resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz",
|
"resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz",
|
||||||
|
@ -10051,6 +10115,37 @@
|
||||||
"uc.micro": "^2.0.0"
|
"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": {
|
"node_modules/loader-runner": {
|
||||||
"version": "4.3.0",
|
"version": "4.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.3.0.tgz",
|
||||||
|
@ -11859,13 +11954,13 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/playwright": {
|
"node_modules/playwright": {
|
||||||
"version": "1.53.0",
|
"version": "1.52.0",
|
||||||
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.53.0.tgz",
|
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.52.0.tgz",
|
||||||
"integrity": "sha512-ghGNnIEYZC4E+YtclRn4/p6oYbdPiASELBIYkBXfaTVKreQUYbMUYQDwS12a8F0/HtIjr/CkGjtwABeFPGcS4Q==",
|
"integrity": "sha512-JAwMNMBlxJ2oD1kce4KPtMkDeKGHQstdpFPcPH3maElAXon/QZeTvtsfXmTMRyO9TslfoYOXkSsvao2nE1ilTw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"playwright-core": "1.53.0"
|
"playwright-core": "1.52.0"
|
||||||
},
|
},
|
||||||
"bin": {
|
"bin": {
|
||||||
"playwright": "cli.js"
|
"playwright": "cli.js"
|
||||||
|
@ -11878,9 +11973,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/playwright-core": {
|
"node_modules/playwright-core": {
|
||||||
"version": "1.53.0",
|
"version": "1.52.0",
|
||||||
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.53.0.tgz",
|
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.52.0.tgz",
|
||||||
"integrity": "sha512-mGLg8m0pm4+mmtB7M89Xw/GSqoNC+twivl8ITteqvAndachozYe2ZA7srU6uleV1vEdAHYqjq+SV8SNxRRFYBw==",
|
"integrity": "sha512-l2osTgLXSMeuLZOML9qYODUQoPPnUsKsb5/P6LJ2e6uPKXUdPK5WYhN4z03G+YNbWmGDY4YENauNu4ZKczreHg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"bin": {
|
"bin": {
|
||||||
|
@ -12363,6 +12458,16 @@
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "Unlicense"
|
"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": {
|
"node_modules/proto-list": {
|
||||||
"version": "1.2.4",
|
"version": "1.2.4",
|
||||||
"resolved": "https://registry.npmjs.org/proto-list/-/proto-list-1.2.4.tgz",
|
"resolved": "https://registry.npmjs.org/proto-list/-/proto-list-1.2.4.tgz",
|
||||||
|
@ -14425,6 +14530,13 @@
|
||||||
"node": ">=0.8"
|
"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": {
|
"node_modules/throttle-debounce": {
|
||||||
"version": "5.0.0",
|
"version": "5.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/throttle-debounce/-/throttle-debounce-5.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/throttle-debounce/-/throttle-debounce-5.0.0.tgz",
|
||||||
|
|
|
@ -11,6 +11,7 @@
|
||||||
"@github/markdown-toolbar-element": "2.2.3",
|
"@github/markdown-toolbar-element": "2.2.3",
|
||||||
"@github/quote-selection": "2.1.0",
|
"@github/quote-selection": "2.1.0",
|
||||||
"@github/text-expander-element": "2.8.0",
|
"@github/text-expander-element": "2.8.0",
|
||||||
|
"@google/model-viewer": "4.1.0",
|
||||||
"@mcaptcha/vanilla-glue": "0.1.0-alpha-3",
|
"@mcaptcha/vanilla-glue": "0.1.0-alpha-3",
|
||||||
"@primer/octicons": "19.14.0",
|
"@primer/octicons": "19.14.0",
|
||||||
"ansi_up": "6.0.5",
|
"ansi_up": "6.0.5",
|
||||||
|
@ -61,7 +62,7 @@
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@axe-core/playwright": "4.10.2",
|
"@axe-core/playwright": "4.10.2",
|
||||||
"@eslint-community/eslint-plugin-eslint-comments": "4.5.0",
|
"@eslint-community/eslint-plugin-eslint-comments": "4.5.0",
|
||||||
"@playwright/test": "1.53.0",
|
"@playwright/test": "1.52.0",
|
||||||
"@stoplight/spectral-cli": "6.15.0",
|
"@stoplight/spectral-cli": "6.15.0",
|
||||||
"@stylistic/eslint-plugin": "4.4.1",
|
"@stylistic/eslint-plugin": "4.4.1",
|
||||||
"@stylistic/stylelint-plugin": "3.1.2",
|
"@stylistic/stylelint-plugin": "3.1.2",
|
||||||
|
@ -78,8 +79,8 @@
|
||||||
"eslint-plugin-playwright": "2.2.0",
|
"eslint-plugin-playwright": "2.2.0",
|
||||||
"eslint-plugin-regexp": "2.9.0",
|
"eslint-plugin-regexp": "2.9.0",
|
||||||
"eslint-plugin-sonarjs": "3.0.2",
|
"eslint-plugin-sonarjs": "3.0.2",
|
||||||
"eslint-plugin-unicorn": "59.0.1",
|
|
||||||
"eslint-plugin-toml": "0.12.0",
|
"eslint-plugin-toml": "0.12.0",
|
||||||
|
"eslint-plugin-unicorn": "59.0.1",
|
||||||
"eslint-plugin-vitest-globals": "1.5.0",
|
"eslint-plugin-vitest-globals": "1.5.0",
|
||||||
"eslint-plugin-vue": "10.2.0",
|
"eslint-plugin-vue": "10.2.0",
|
||||||
"eslint-plugin-vue-scoped-css": "2.10.0",
|
"eslint-plugin-vue-scoped-css": "2.10.0",
|
||||||
|
|
|
@ -748,7 +748,7 @@ func ListActionRuns(ctx *context.APIContext) {
|
||||||
// type: string
|
// type: string
|
||||||
// responses:
|
// responses:
|
||||||
// "200":
|
// "200":
|
||||||
// "$ref": "#/responses/RepoActionRunList"
|
// "$ref": "#/responses/ActionRunList"
|
||||||
// "400":
|
// "400":
|
||||||
// "$ref": "#/responses/error"
|
// "$ref": "#/responses/error"
|
||||||
// "403":
|
// "403":
|
||||||
|
@ -779,16 +779,16 @@ func ListActionRuns(ctx *context.APIContext) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
res := new(api.ListRepoActionRunResponse)
|
res := new(api.ListActionRunResponse)
|
||||||
res.TotalCount = total
|
res.TotalCount = total
|
||||||
|
|
||||||
res.Entries = make([]*api.RepoActionRun, len(runs))
|
res.Entries = make([]*api.ActionRun, len(runs))
|
||||||
for i, r := range runs {
|
for i, r := range runs {
|
||||||
cr, err := convert.ToRepoActionRun(ctx, r)
|
if err := r.LoadAttributes(ctx); err != nil {
|
||||||
if err != nil {
|
ctx.Error(http.StatusInternalServerError, "LoadAttributes", err)
|
||||||
ctx.Error(http.StatusInternalServerError, "ToActionRun", err)
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
cr := convert.ToActionRun(ctx, r, ctx.Doer)
|
||||||
res.Entries[i] = cr
|
res.Entries[i] = cr
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -821,7 +821,7 @@ func GetActionRun(ctx *context.APIContext) {
|
||||||
// required: true
|
// required: true
|
||||||
// responses:
|
// responses:
|
||||||
// "200":
|
// "200":
|
||||||
// "$ref": "#/responses/RepoActionRun"
|
// "$ref": "#/responses/ActionRun"
|
||||||
// "400":
|
// "400":
|
||||||
// "$ref": "#/responses/error"
|
// "$ref": "#/responses/error"
|
||||||
// "403":
|
// "403":
|
||||||
|
@ -839,16 +839,17 @@ func GetActionRun(ctx *context.APIContext) {
|
||||||
return
|
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 {
|
if ctx.Repo.Repository.ID != run.RepoID {
|
||||||
ctx.Error(http.StatusNotFound, "GetRunById", util.ErrNotExist)
|
ctx.Error(http.StatusNotFound, "GetRunById", util.ErrNotExist)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
res, err := convert.ToRepoActionRun(ctx, run)
|
if err := run.LoadAttributes(ctx); err != nil {
|
||||||
if err != nil {
|
ctx.Error(http.StatusInternalServerError, "LoadAttributes", err)
|
||||||
ctx.Error(http.StatusInternalServerError, "ToRepoActionRun", err)
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx.JSON(http.StatusOK, res)
|
ctx.JSON(http.StatusOK, convert.ToActionRun(ctx, run, ctx.Doer))
|
||||||
}
|
}
|
||||||
|
|
|
@ -463,16 +463,16 @@ type swaggerSyncForkInfo struct {
|
||||||
Body []api.SyncForkInfo `json:"body"`
|
Body []api.SyncForkInfo `json:"body"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// RepoActionRunList
|
// ActionRunList
|
||||||
// swagger:response RepoActionRunList
|
// swagger:response ActionRunList
|
||||||
type swaggerRepoActionRunList struct {
|
type swaggerActionRunList struct {
|
||||||
// in:body
|
// in:body
|
||||||
Body api.ListRepoActionRunResponse `json:"body"`
|
Body api.ListActionRunResponse `json:"body"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// RepoActionRun
|
// ActionRun
|
||||||
// swagger:response RepoActionRun
|
// swagger:response ActionRun
|
||||||
type swaggerRepoActionRun struct {
|
type swaggerActionRun struct {
|
||||||
// in:body
|
// in:body
|
||||||
Body api.RepoActionRun `json:"body"`
|
Body api.ActionRun `json:"body"`
|
||||||
}
|
}
|
||||||
|
|
|
@ -175,10 +175,12 @@ func prepareOrgProfileReadme(ctx *context.Context, profileGitRepo *git.Repositor
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if bytes, err := profileReadme.GetBlobContent(setting.UI.MaxDisplayFileSize); err != nil {
|
if rc, _, err := profileReadme.NewTruncatedReader(setting.UI.MaxDisplayFileSize); err != nil {
|
||||||
log.Error("failed to GetBlobContent: %v", err)
|
log.Error("failed to NewTruncatedReader: %v", err)
|
||||||
} else {
|
} else {
|
||||||
if profileContent, err := markdown.RenderString(&markup.RenderContext{
|
defer rc.Close()
|
||||||
|
|
||||||
|
if profileContent, err := markdown.RenderReader(&markup.RenderContext{
|
||||||
Ctx: ctx,
|
Ctx: ctx,
|
||||||
GitRepo: profileGitRepo,
|
GitRepo: profileGitRepo,
|
||||||
Links: markup.Links{
|
Links: markup.Links{
|
||||||
|
@ -188,7 +190,7 @@ func prepareOrgProfileReadme(ctx *context.Context, profileGitRepo *git.Repositor
|
||||||
BranchPath: path.Join("branch", util.PathEscapeSegments(profileDbRepo.DefaultBranch)),
|
BranchPath: path.Join("branch", util.PathEscapeSegments(profileDbRepo.DefaultBranch)),
|
||||||
},
|
},
|
||||||
Metas: map[string]string{"mode": "document"},
|
Metas: map[string]string{"mode": "document"},
|
||||||
}, bytes); err != nil {
|
}, rc); err != nil {
|
||||||
log.Error("failed to RenderString: %v", err)
|
log.Error("failed to RenderString: %v", err)
|
||||||
} else {
|
} else {
|
||||||
ctx.Data["ProfileReadme"] = profileContent
|
ctx.Data["ProfileReadme"] = profileContent
|
||||||
|
|
|
@ -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.
|
// Special user that can't have associated contributions and permissions in the repo.
|
||||||
if poster.IsGhost() || poster.IsActions() || poster.IsAPServerActor() {
|
if poster.IsSystem() || poster.IsAPServerActor() {
|
||||||
return roleDescriptor, nil
|
return roleDescriptor, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -342,6 +342,20 @@ func LFSFileGet(ctx *context.Context) {
|
||||||
ctx.Data["IsVideoFile"] = true
|
ctx.Data["IsVideoFile"] = true
|
||||||
case st.IsAudio():
|
case st.IsAudio():
|
||||||
ctx.Data["IsAudioFile"] = true
|
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()):
|
case st.IsImage() && (setting.UI.SVG.Enabled || !st.IsSvgImage()):
|
||||||
ctx.Data["IsImageFile"] = true
|
ctx.Data["IsImageFile"] = true
|
||||||
}
|
}
|
||||||
|
|
|
@ -153,11 +153,9 @@ func UnitsPost(ctx *context.Context) {
|
||||||
})
|
})
|
||||||
deleteUnitTypes = append(deleteUnitTypes, unit_model.TypeWiki)
|
deleteUnitTypes = append(deleteUnitTypes, unit_model.TypeWiki)
|
||||||
} else if form.EnableWiki && !form.EnableExternalWiki && !unit_model.TypeWiki.UnitGlobalDisabled() {
|
} else if form.EnableWiki && !form.EnableExternalWiki && !unit_model.TypeWiki.UnitGlobalDisabled() {
|
||||||
var wikiPermissions repo_model.UnitAccessMode
|
wikiPermissions := repo_model.UnitAccessModeUnset
|
||||||
if form.GloballyWriteableWiki {
|
if form.GloballyWriteableWiki {
|
||||||
wikiPermissions = repo_model.UnitAccessModeWrite
|
wikiPermissions = repo_model.UnitAccessModeWrite
|
||||||
} else {
|
|
||||||
wikiPermissions = repo_model.UnitAccessModeRead
|
|
||||||
}
|
}
|
||||||
units = append(units, repo_model.RepoUnit{
|
units = append(units, repo_model.RepoUnit{
|
||||||
RepoID: repo.ID,
|
RepoID: repo.ID,
|
||||||
|
|
|
@ -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())
|
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) {
|
} else if slices.Contains([]string{"CODEOWNERS", "docs/CODEOWNERS", ".gitea/CODEOWNERS"}, ctx.Repo.TreePath) {
|
||||||
if data, err := blob.GetBlobContent(setting.UI.MaxDisplayFileSize); err == nil {
|
if rc, size, err := blob.NewTruncatedReader(setting.UI.MaxDisplayFileSize); err == nil {
|
||||||
_, warnings := issue_model.GetCodeOwnersFromContent(ctx, data)
|
_, warnings := issue_model.GetCodeOwnersFromReader(ctx, rc, size > setting.UI.MaxDisplayFileSize)
|
||||||
if len(warnings) > 0 {
|
if len(warnings) > 0 {
|
||||||
ctx.Data["FileWarning"] = strings.Join(warnings, "\n")
|
ctx.Data["FileWarning"] = strings.Join(warnings, "\n")
|
||||||
}
|
}
|
||||||
|
@ -624,6 +624,20 @@ func renderFile(ctx *context.Context, entry *git.TreeEntry) {
|
||||||
ctx.Data["IsVideoFile"] = true
|
ctx.Data["IsVideoFile"] = true
|
||||||
case fInfo.st.IsAudio():
|
case fInfo.st.IsAudio():
|
||||||
ctx.Data["IsAudioFile"] = true
|
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()):
|
case fInfo.st.IsImage() && (setting.UI.SVG.Enabled || !fInfo.st.IsSvgImage()):
|
||||||
ctx.Data["IsImageFile"] = true
|
ctx.Data["IsImageFile"] = true
|
||||||
ctx.Data["CanCopyContent"] = true
|
ctx.Data["CanCopyContent"] = true
|
||||||
|
|
|
@ -264,10 +264,12 @@ func prepareUserProfileTabData(ctx *context.Context, showPrivate bool, profileDb
|
||||||
|
|
||||||
total = int(count)
|
total = int(count)
|
||||||
case "overview":
|
case "overview":
|
||||||
if bytes, err := profileReadme.GetBlobContent(setting.UI.MaxDisplayFileSize); err != nil {
|
if rc, _, err := profileReadme.NewTruncatedReader(setting.UI.MaxDisplayFileSize); err != nil {
|
||||||
log.Error("failed to GetBlobContent: %v", err)
|
log.Error("failed to NewTruncatedReader: %v", err)
|
||||||
} else {
|
} else {
|
||||||
if profileContent, err := markdown.RenderString(&markup.RenderContext{
|
defer rc.Close()
|
||||||
|
|
||||||
|
if profileContent, err := markdown.RenderReader(&markup.RenderContext{
|
||||||
Ctx: ctx,
|
Ctx: ctx,
|
||||||
GitRepo: profileGitRepo,
|
GitRepo: profileGitRepo,
|
||||||
Links: markup.Links{
|
Links: markup.Links{
|
||||||
|
@ -280,7 +282,7 @@ func prepareUserProfileTabData(ctx *context.Context, showPrivate bool, profileDb
|
||||||
BranchPath: path.Join("branch", util.PathEscapeSegments(profileDbRepo.DefaultBranch)),
|
BranchPath: path.Join("branch", util.PathEscapeSegments(profileDbRepo.DefaultBranch)),
|
||||||
},
|
},
|
||||||
Metas: map[string]string{"mode": "document"},
|
Metas: map[string]string{"mode": "document"},
|
||||||
}, bytes); err != nil {
|
}, rc); err != nil {
|
||||||
log.Error("failed to RenderString: %v", err)
|
log.Error("failed to RenderString: %v", err)
|
||||||
} else {
|
} else {
|
||||||
ctx.Data["ProfileReadme"] = profileContent
|
ctx.Data["ProfileReadme"] = profileContent
|
||||||
|
|
|
@ -66,6 +66,9 @@ func ProfilePost(ctx *context.Context) {
|
||||||
ctx.Data["DisableGravatar"] = setting.Config().Picture.DisableGravatar.Value(ctx)
|
ctx.Data["DisableGravatar"] = setting.Config().Picture.DisableGravatar.Value(ctx)
|
||||||
ctx.Data["CooldownPeriod"] = setting.Service.UsernameCooldownPeriod
|
ctx.Data["CooldownPeriod"] = setting.Service.UsernameCooldownPeriod
|
||||||
ctx.Data["CommonPronouns"] = commonPronouns
|
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() {
|
if ctx.HasError() {
|
||||||
ctx.HTML(http.StatusOK, tplSettingsProfile)
|
ctx.HTML(http.StatusOK, tplSettingsProfile)
|
||||||
|
|
|
@ -345,6 +345,14 @@ func handleWorkflows(
|
||||||
Status: actions_model.StatusWaiting,
|
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)
|
need, err := ifNeedApproval(ctx, run, input.Repo, input.Doer)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error("check if need approval for repo %d with user %d: %v", input.Repo.ID, input.Doer.ID, err)
|
log.Error("check if need approval for repo %d with user %d: %v", input.Repo.ID, input.Doer.ID, err)
|
||||||
|
|
|
@ -4,6 +4,7 @@
|
||||||
package actions
|
package actions
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
@ -18,6 +19,7 @@ import (
|
||||||
webhook_module "forgejo.org/modules/webhook"
|
webhook_module "forgejo.org/modules/webhook"
|
||||||
|
|
||||||
"github.com/nektos/act/pkg/jobparser"
|
"github.com/nektos/act/pkg/jobparser"
|
||||||
|
act_model "github.com/nektos/act/pkg/model"
|
||||||
"xorm.io/builder"
|
"xorm.io/builder"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -140,6 +142,16 @@ func CreateScheduleTask(ctx context.Context, cron *actions_model.ActionSchedule)
|
||||||
return err
|
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
|
// Parse the workflow specification from the cron schedule
|
||||||
workflows, err := jobparser.Parse(cron.Content, jobparser.WithVars(vars))
|
workflows, err := jobparser.Parse(cron.Content, jobparser.WithVars(vars))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
121
services/actions/schedule_tasks_test.go
Normal file
121
services/actions/schedule_tasks_test.go
Normal file
|
@ -0,0 +1,121 @@
|
||||||
|
// 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})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
|
@ -111,6 +111,11 @@ func (entry *Workflow) Dispatch(ctx context.Context, inputGetter InputValueGette
|
||||||
return nil, nil, err
|
return nil, nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
notifications, err := wf.Notifications()
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
run := &actions_model.ActionRun{
|
run := &actions_model.ActionRun{
|
||||||
Title: title,
|
Title: title,
|
||||||
RepoID: repo.ID,
|
RepoID: repo.ID,
|
||||||
|
@ -125,6 +130,7 @@ func (entry *Workflow) Dispatch(ctx context.Context, inputGetter InputValueGette
|
||||||
EventPayload: string(p),
|
EventPayload: string(p),
|
||||||
TriggerEvent: string(webhook.HookEventWorkflowDispatch),
|
TriggerEvent: string(webhook.HookEventWorkflowDispatch),
|
||||||
Status: actions_model.StatusWaiting,
|
Status: actions_model.StatusWaiting,
|
||||||
|
NotifyEmail: notifications,
|
||||||
}
|
}
|
||||||
|
|
||||||
vars, err := actions_model.GetVariablesOfRun(ctx, run)
|
vars, err := actions_model.GetVariablesOfRun(ctx, run)
|
||||||
|
|
|
@ -8,22 +8,17 @@ import (
|
||||||
|
|
||||||
actions_model "forgejo.org/models/actions"
|
actions_model "forgejo.org/models/actions"
|
||||||
access_model "forgejo.org/models/perm/access"
|
access_model "forgejo.org/models/perm/access"
|
||||||
|
user_model "forgejo.org/models/user"
|
||||||
api "forgejo.org/modules/structs"
|
api "forgejo.org/modules/structs"
|
||||||
)
|
)
|
||||||
|
|
||||||
// ToActionRun convert actions_model.User to api.ActionRun
|
// ToActionRun convert actions_model.User to api.ActionRun
|
||||||
// the run needs all attributes loaded
|
// the run needs all attributes loaded
|
||||||
func ToActionRun(ctx context.Context, run *actions_model.ActionRun) *api.ActionRun {
|
func ToActionRun(ctx context.Context, run *actions_model.ActionRun, doer *user_model.User) *api.ActionRun {
|
||||||
if run == nil {
|
if run == nil {
|
||||||
return 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)
|
permissionInRepo, _ := access_model.GetUserRepoPermission(ctx, run.Repo, doer)
|
||||||
|
|
||||||
return &api.ActionRun{
|
return &api.ActionRun{
|
||||||
|
|
|
@ -222,29 +222,6 @@ func ToActionTask(ctx context.Context, t *actions_model.ActionTask) (*api.Action
|
||||||
}, nil
|
}, 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
|
// ToVerification convert a git.Commit.Signature to an api.PayloadCommitVerification
|
||||||
func ToVerification(ctx context.Context, c *git.Commit) *api.PayloadCommitVerification {
|
func ToVerification(ctx context.Context, c *git.Commit) *api.PayloadCommitVerification {
|
||||||
verif := asymkey_model.ParseCommitWithSignature(ctx, c)
|
verif := asymkey_model.ParseCommitWithSignature(ctx, c)
|
||||||
|
|
|
@ -211,6 +211,11 @@ func CreateUserFromAP(ctx context.Context, personID fm.PersonID, federationHostI
|
||||||
return nil, nil, err
|
return nil, nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
inbox, err := url.ParseRequestURI(person.Inbox.GetLink().String())
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
newUser := user.User{
|
newUser := user.User{
|
||||||
LowerName: strings.ToLower(name),
|
LowerName: strings.ToLower(name),
|
||||||
Name: name,
|
Name: name,
|
||||||
|
@ -227,6 +232,7 @@ func CreateUserFromAP(ctx context.Context, personID fm.PersonID, federationHostI
|
||||||
federatedUser := user.FederatedUser{
|
federatedUser := user.FederatedUser{
|
||||||
ExternalID: personID.ID,
|
ExternalID: personID.ID,
|
||||||
FederationHostID: federationHostID,
|
FederationHostID: federationHostID,
|
||||||
|
InboxPath: inbox.Path,
|
||||||
NormalizedOriginalURL: personID.AsURI(),
|
NormalizedOriginalURL: personID.AsURI(),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -39,6 +39,24 @@ func NewNotifier() notify_service.Notifier {
|
||||||
return &actionNotifier{}
|
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) {
|
func (a *actionNotifier) NewIssue(ctx context.Context, issue *issues_model.Issue, mentions []*user_model.User) {
|
||||||
if err := issue.LoadPoster(ctx); err != nil {
|
if err := issue.LoadPoster(ctx); err != nil {
|
||||||
log.Error("issue.LoadPoster: %v", err)
|
log.Error("issue.LoadPoster: %v", err)
|
||||||
|
@ -50,7 +68,7 @@ func (a *actionNotifier) NewIssue(ctx context.Context, issue *issues_model.Issue
|
||||||
}
|
}
|
||||||
repo := issue.Repo
|
repo := issue.Repo
|
||||||
|
|
||||||
if err := activities_model.NotifyWatchers(ctx, &activities_model.Action{
|
if err := notifyAll(ctx, &activities_model.Action{
|
||||||
ActUserID: issue.Poster.ID,
|
ActUserID: issue.Poster.ID,
|
||||||
ActUser: issue.Poster,
|
ActUser: issue.Poster,
|
||||||
OpType: activities_model.ActionCreateIssue,
|
OpType: activities_model.ActionCreateIssue,
|
||||||
|
@ -91,7 +109,7 @@ func (a *actionNotifier) IssueChangeStatus(ctx context.Context, doer *user_model
|
||||||
}
|
}
|
||||||
|
|
||||||
// Notify watchers for whatever action comes in, ignore if no action type.
|
// Notify watchers for whatever action comes in, ignore if no action type.
|
||||||
if err := activities_model.NotifyWatchers(ctx, act); err != nil {
|
if err := notifyAll(ctx, act); err != nil {
|
||||||
log.Error("NotifyWatchers: %v", err)
|
log.Error("NotifyWatchers: %v", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -127,7 +145,7 @@ func (a *actionNotifier) CreateIssueComment(ctx context.Context, doer *user_mode
|
||||||
}
|
}
|
||||||
|
|
||||||
// Notify watchers for whatever action comes in, ignore if no action type.
|
// Notify watchers for whatever action comes in, ignore if no action type.
|
||||||
if err := activities_model.NotifyWatchers(ctx, act); err != nil {
|
if err := notifyAll(ctx, act); err != nil {
|
||||||
log.Error("NotifyWatchers: %v", err)
|
log.Error("NotifyWatchers: %v", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -146,7 +164,7 @@ func (a *actionNotifier) NewPullRequest(ctx context.Context, pull *issues_model.
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := activities_model.NotifyWatchers(ctx, &activities_model.Action{
|
if err := notifyAll(ctx, &activities_model.Action{
|
||||||
ActUserID: pull.Issue.Poster.ID,
|
ActUserID: pull.Issue.Poster.ID,
|
||||||
ActUser: pull.Issue.Poster,
|
ActUser: pull.Issue.Poster,
|
||||||
OpType: activities_model.ActionCreatePullRequest,
|
OpType: activities_model.ActionCreatePullRequest,
|
||||||
|
@ -160,7 +178,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) {
|
func (a *actionNotifier) RenameRepository(ctx context.Context, doer *user_model.User, repo *repo_model.Repository, oldRepoName string) {
|
||||||
if err := activities_model.NotifyWatchers(ctx, &activities_model.Action{
|
if err := notifyAll(ctx, &activities_model.Action{
|
||||||
ActUserID: doer.ID,
|
ActUserID: doer.ID,
|
||||||
ActUser: doer,
|
ActUser: doer,
|
||||||
OpType: activities_model.ActionRenameRepo,
|
OpType: activities_model.ActionRenameRepo,
|
||||||
|
@ -174,7 +192,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) {
|
func (a *actionNotifier) TransferRepository(ctx context.Context, doer *user_model.User, repo *repo_model.Repository, oldOwnerName string) {
|
||||||
if err := activities_model.NotifyWatchers(ctx, &activities_model.Action{
|
if err := notifyAll(ctx, &activities_model.Action{
|
||||||
ActUserID: doer.ID,
|
ActUserID: doer.ID,
|
||||||
ActUser: doer,
|
ActUser: doer,
|
||||||
OpType: activities_model.ActionTransferRepo,
|
OpType: activities_model.ActionTransferRepo,
|
||||||
|
@ -188,7 +206,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) {
|
func (a *actionNotifier) CreateRepository(ctx context.Context, doer, u *user_model.User, repo *repo_model.Repository) {
|
||||||
if err := activities_model.NotifyWatchers(ctx, &activities_model.Action{
|
if err := notifyAll(ctx, &activities_model.Action{
|
||||||
ActUserID: doer.ID,
|
ActUserID: doer.ID,
|
||||||
ActUser: doer,
|
ActUser: doer,
|
||||||
OpType: activities_model.ActionCreateRepo,
|
OpType: activities_model.ActionCreateRepo,
|
||||||
|
@ -201,7 +219,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) {
|
func (a *actionNotifier) ForkRepository(ctx context.Context, doer *user_model.User, oldRepo, repo *repo_model.Repository) {
|
||||||
if err := activities_model.NotifyWatchers(ctx, &activities_model.Action{
|
if err := notifyAll(ctx, &activities_model.Action{
|
||||||
ActUserID: doer.ID,
|
ActUserID: doer.ID,
|
||||||
ActUser: doer,
|
ActUser: doer,
|
||||||
OpType: activities_model.ActionCreateRepo,
|
OpType: activities_model.ActionCreateRepo,
|
||||||
|
@ -266,13 +284,13 @@ func (a *actionNotifier) PullRequestReview(ctx context.Context, pr *issues_model
|
||||||
actions = append(actions, action)
|
actions = append(actions, action)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := activities_model.NotifyWatchersActions(ctx, actions); err != nil {
|
if err := notifyAllActions(ctx, actions); err != nil {
|
||||||
log.Error("notify watchers '%d/%d': %v", review.Reviewer.ID, review.Issue.RepoID, err)
|
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) {
|
func (*actionNotifier) MergePullRequest(ctx context.Context, doer *user_model.User, pr *issues_model.PullRequest) {
|
||||||
if err := activities_model.NotifyWatchers(ctx, &activities_model.Action{
|
if err := notifyAll(ctx, &activities_model.Action{
|
||||||
ActUserID: doer.ID,
|
ActUserID: doer.ID,
|
||||||
ActUser: doer,
|
ActUser: doer,
|
||||||
OpType: activities_model.ActionMergePullRequest,
|
OpType: activities_model.ActionMergePullRequest,
|
||||||
|
@ -286,7 +304,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) {
|
func (*actionNotifier) AutoMergePullRequest(ctx context.Context, doer *user_model.User, pr *issues_model.PullRequest) {
|
||||||
if err := activities_model.NotifyWatchers(ctx, &activities_model.Action{
|
if err := notifyAll(ctx, &activities_model.Action{
|
||||||
ActUserID: doer.ID,
|
ActUserID: doer.ID,
|
||||||
ActUser: doer,
|
ActUser: doer,
|
||||||
OpType: activities_model.ActionAutoMergePullRequest,
|
OpType: activities_model.ActionAutoMergePullRequest,
|
||||||
|
@ -304,7 +322,7 @@ func (*actionNotifier) NotifyPullRevieweDismiss(ctx context.Context, doer *user_
|
||||||
if len(review.OriginalAuthor) > 0 {
|
if len(review.OriginalAuthor) > 0 {
|
||||||
reviewerName = review.OriginalAuthor
|
reviewerName = review.OriginalAuthor
|
||||||
}
|
}
|
||||||
if err := activities_model.NotifyWatchers(ctx, &activities_model.Action{
|
if err := notifyAll(ctx, &activities_model.Action{
|
||||||
ActUserID: doer.ID,
|
ActUserID: doer.ID,
|
||||||
ActUser: doer,
|
ActUser: doer,
|
||||||
OpType: activities_model.ActionPullReviewDismissed,
|
OpType: activities_model.ActionPullReviewDismissed,
|
||||||
|
@ -342,7 +360,7 @@ func (a *actionNotifier) PushCommits(ctx context.Context, pusher *user_model.Use
|
||||||
opType = activities_model.ActionDeleteBranch
|
opType = activities_model.ActionDeleteBranch
|
||||||
}
|
}
|
||||||
|
|
||||||
if err = activities_model.NotifyWatchers(ctx, &activities_model.Action{
|
if err = notifyAll(ctx, &activities_model.Action{
|
||||||
ActUserID: pusher.ID,
|
ActUserID: pusher.ID,
|
||||||
ActUser: pusher,
|
ActUser: pusher,
|
||||||
OpType: opType,
|
OpType: opType,
|
||||||
|
@ -362,7 +380,7 @@ func (a *actionNotifier) CreateRef(ctx context.Context, doer *user_model.User, r
|
||||||
// has sent same action in `PushCommits`, so skip it.
|
// has sent same action in `PushCommits`, so skip it.
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if err := activities_model.NotifyWatchers(ctx, &activities_model.Action{
|
if err := notifyAll(ctx, &activities_model.Action{
|
||||||
ActUserID: doer.ID,
|
ActUserID: doer.ID,
|
||||||
ActUser: doer,
|
ActUser: doer,
|
||||||
OpType: opType,
|
OpType: opType,
|
||||||
|
@ -381,7 +399,7 @@ func (a *actionNotifier) DeleteRef(ctx context.Context, doer *user_model.User, r
|
||||||
// has sent same action in `PushCommits`, so skip it.
|
// has sent same action in `PushCommits`, so skip it.
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if err := activities_model.NotifyWatchers(ctx, &activities_model.Action{
|
if err := notifyAll(ctx, &activities_model.Action{
|
||||||
ActUserID: doer.ID,
|
ActUserID: doer.ID,
|
||||||
ActUser: doer,
|
ActUser: doer,
|
||||||
OpType: opType,
|
OpType: opType,
|
||||||
|
@ -405,7 +423,7 @@ func (a *actionNotifier) SyncPushCommits(ctx context.Context, pusher *user_model
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := activities_model.NotifyWatchers(ctx, &activities_model.Action{
|
if err := notifyAll(ctx, &activities_model.Action{
|
||||||
ActUserID: repo.OwnerID,
|
ActUserID: repo.OwnerID,
|
||||||
ActUser: repo.MustOwner(ctx),
|
ActUser: repo.MustOwner(ctx),
|
||||||
OpType: activities_model.ActionMirrorSyncPush,
|
OpType: activities_model.ActionMirrorSyncPush,
|
||||||
|
@ -420,7 +438,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) {
|
func (a *actionNotifier) SyncCreateRef(ctx context.Context, doer *user_model.User, repo *repo_model.Repository, refFullName git.RefName, refID string) {
|
||||||
if err := activities_model.NotifyWatchers(ctx, &activities_model.Action{
|
if err := notifyAll(ctx, &activities_model.Action{
|
||||||
ActUserID: repo.OwnerID,
|
ActUserID: repo.OwnerID,
|
||||||
ActUser: repo.MustOwner(ctx),
|
ActUser: repo.MustOwner(ctx),
|
||||||
OpType: activities_model.ActionMirrorSyncCreate,
|
OpType: activities_model.ActionMirrorSyncCreate,
|
||||||
|
@ -434,7 +452,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) {
|
func (a *actionNotifier) SyncDeleteRef(ctx context.Context, doer *user_model.User, repo *repo_model.Repository, refFullName git.RefName) {
|
||||||
if err := activities_model.NotifyWatchers(ctx, &activities_model.Action{
|
if err := notifyAll(ctx, &activities_model.Action{
|
||||||
ActUserID: repo.OwnerID,
|
ActUserID: repo.OwnerID,
|
||||||
ActUser: repo.MustOwner(ctx),
|
ActUser: repo.MustOwner(ctx),
|
||||||
OpType: activities_model.ActionMirrorSyncDelete,
|
OpType: activities_model.ActionMirrorSyncDelete,
|
||||||
|
@ -452,7 +470,7 @@ func (a *actionNotifier) NewRelease(ctx context.Context, rel *repo_model.Release
|
||||||
log.Error("LoadAttributes: %v", err)
|
log.Error("LoadAttributes: %v", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if err := activities_model.NotifyWatchers(ctx, &activities_model.Action{
|
if err := notifyAll(ctx, &activities_model.Action{
|
||||||
ActUserID: rel.PublisherID,
|
ActUserID: rel.PublisherID,
|
||||||
ActUser: rel.Publisher,
|
ActUser: rel.Publisher,
|
||||||
OpType: activities_model.ActionPublishRelease,
|
OpType: activities_model.ActionPublishRelease,
|
||||||
|
|
|
@ -43,8 +43,6 @@ type ReviewRequestNotifier struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
func PullRequestCodeOwnersReview(ctx context.Context, issue *issues_model.Issue, pr *issues_model.PullRequest) ([]*ReviewRequestNotifier, error) {
|
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) {
|
if pr.IsWorkInProgress(ctx) {
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
@ -72,18 +70,17 @@ func PullRequestCodeOwnersReview(ctx context.Context, issue *issues_model.Issue,
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
var data string
|
var rules []*issues_model.CodeOwnerRule
|
||||||
for _, file := range files {
|
for _, file := range []string{"CODEOWNERS", "docs/CODEOWNERS", ".gitea/CODEOWNERS"} {
|
||||||
if blob, err := commit.GetBlobByPath(file); err == nil {
|
if blob, err := commit.GetBlobByPath(file); err == nil {
|
||||||
data, err = blob.GetBlobContent(setting.UI.MaxDisplayFileSize)
|
rc, size, err := blob.NewTruncatedReader(setting.UI.MaxDisplayFileSize)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
|
rules, _ = issues_model.GetCodeOwnersFromReader(ctx, rc, size > setting.UI.MaxDisplayFileSize)
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
rules, _ := issues_model.GetCodeOwnersFromContent(ctx, data)
|
|
||||||
|
|
||||||
// get the mergebase
|
// get the mergebase
|
||||||
mergeBase, err := getMergeBase(repo, pr, git.BranchPrefix+pr.BaseBranch, pr.GetGitRefName())
|
mergeBase, err := getMergeBase(repo, pr, git.BranchPrefix+pr.BaseBranch, pr.GetGitRefName())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
|
@ -23,19 +23,24 @@ func MailActionRun(run *actions_model.ActionRun, priorStatus actions_model.Statu
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
if run.TriggerUser.Email != "" && run.TriggerUser.EmailNotificationsPreference != user_model.EmailNotificationsDisabled {
|
if !run.NotifyEmail {
|
||||||
if err := sendMailActionRun(run.TriggerUser, run, priorStatus, lastRun); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
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 user.EmailNotificationsPreference == user_model.EmailNotificationsDisabled {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return sendMailActionRun(user, run, priorStatus, lastRun)
|
||||||
}
|
}
|
||||||
|
|
||||||
func sendMailActionRun(to *user_model.User, run *actions_model.ActionRun, priorStatus actions_model.Status, lastRun *actions_model.ActionRun) error {
|
func sendMailActionRun(to *user_model.User, run *actions_model.ActionRun, priorStatus actions_model.Status, lastRun *actions_model.ActionRun) error {
|
||||||
|
|
|
@ -4,42 +4,53 @@
|
||||||
package mailer
|
package mailer
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"slices"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
actions_model "forgejo.org/models/actions"
|
actions_model "forgejo.org/models/actions"
|
||||||
"forgejo.org/models/db"
|
"forgejo.org/models/db"
|
||||||
|
organization_model "forgejo.org/models/organization"
|
||||||
repo_model "forgejo.org/models/repo"
|
repo_model "forgejo.org/models/repo"
|
||||||
user_model "forgejo.org/models/user"
|
user_model "forgejo.org/models/user"
|
||||||
|
"forgejo.org/modules/optional"
|
||||||
"forgejo.org/modules/setting"
|
"forgejo.org/modules/setting"
|
||||||
|
"forgejo.org/modules/test"
|
||||||
notify_service "forgejo.org/services/notify"
|
notify_service "forgejo.org/services/notify"
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
|
|
||||||
func getActionsNowDoneTestUsers(t *testing.T) []*user_model.User {
|
func getActionsNowDoneTestUser(t *testing.T, name, email, notifications string) *user_model.User {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
newTriggerUser := new(user_model.User)
|
user := new(user_model.User)
|
||||||
newTriggerUser.Name = "new_trigger_user"
|
user.Name = name
|
||||||
newTriggerUser.Language = "en_US"
|
user.Language = "en_US"
|
||||||
newTriggerUser.IsAdmin = false
|
user.IsAdmin = false
|
||||||
newTriggerUser.Email = "new_trigger_user@example.com"
|
user.Email = email
|
||||||
newTriggerUser.LastLoginUnix = 1693648327
|
user.LastLoginUnix = 1693648327
|
||||||
newTriggerUser.CreatedUnix = 1693648027
|
user.CreatedUnix = 1693648027
|
||||||
newTriggerUser.EmailNotificationsPreference = user_model.EmailNotificationsEnabled
|
opts := user_model.CreateUserOverwriteOptions{
|
||||||
require.NoError(t, user_model.CreateUser(db.DefaultContext, newTriggerUser))
|
AllowCreateOrganization: optional.Some(true),
|
||||||
|
EmailNotificationsPreference: ¬ifications,
|
||||||
|
}
|
||||||
|
require.NoError(t, user_model.AdminCreateUser(db.DefaultContext, user, &opts))
|
||||||
|
return user
|
||||||
|
}
|
||||||
|
|
||||||
newOwner := new(user_model.User)
|
func getActionsNowDoneTestOrg(t *testing.T, name, email string, owner *user_model.User) *user_model.User {
|
||||||
newOwner.Name = "new_owner"
|
t.Helper()
|
||||||
newOwner.Language = "en_US"
|
org := new(organization_model.Organization)
|
||||||
newOwner.IsAdmin = false
|
org.Name = name
|
||||||
newOwner.Email = "new_owner@example.com"
|
org.Language = "en_US"
|
||||||
newOwner.LastLoginUnix = 1693648329
|
org.IsAdmin = false
|
||||||
newOwner.CreatedUnix = 1693648029
|
// contact email for the organization, for display purposes but otherwise not used as of v12
|
||||||
newOwner.EmailNotificationsPreference = user_model.EmailNotificationsEnabled
|
org.Email = email
|
||||||
require.NoError(t, user_model.CreateUser(db.DefaultContext, newOwner))
|
org.LastLoginUnix = 1693648327
|
||||||
|
org.CreatedUnix = 1693648027
|
||||||
return []*user_model.User{newTriggerUser, newOwner}
|
org.Email = email
|
||||||
|
require.NoError(t, organization_model.CreateOrganization(db.DefaultContext, org, owner))
|
||||||
|
return (*user_model.User)(org)
|
||||||
}
|
}
|
||||||
|
|
||||||
func assertTranslatedLocaleMailActionsNowDone(t *testing.T, msgBody string) {
|
func assertTranslatedLocaleMailActionsNowDone(t *testing.T, msgBody string) {
|
||||||
|
@ -49,51 +60,139 @@ func assertTranslatedLocaleMailActionsNowDone(t *testing.T, msgBody string) {
|
||||||
func TestActionRunNowDoneNotificationMail(t *testing.T) {
|
func TestActionRunNowDoneNotificationMail(t *testing.T) {
|
||||||
ctx := t.Context()
|
ctx := t.Context()
|
||||||
|
|
||||||
users := getActionsNowDoneTestUsers(t)
|
defer test.MockVariableValue(&setting.Admin.DisableRegularOrgCreation, false)()
|
||||||
defer CleanUpUsers(ctx, users)
|
|
||||||
triggerUser := users[0]
|
actionsUser := user_model.NewActionsUser()
|
||||||
ownerUser := users[1]
|
require.NotEmpty(t, actionsUser.Email)
|
||||||
|
|
||||||
repo := repo_model.Repository{
|
repo := repo_model.Repository{
|
||||||
Name: "some repo",
|
Name: "some repo",
|
||||||
Description: "rockets are cool",
|
Description: "rockets are cool",
|
||||||
Owner: ownerUser,
|
|
||||||
OwnerID: ownerUser.ID,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Do some funky stuff with the action run's ids:
|
// Do some funky stuff with the action run's ids:
|
||||||
// The run with the larger ID finished first.
|
// The run with the larger ID finished first.
|
||||||
// This is odd but something that must work.
|
// This is odd but something that must work.
|
||||||
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"}
|
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", TriggerUser: triggerUser, TriggerUserID: triggerUser.ID, Status: actions_model.StatusSuccess, Stopped: 1745822796, TriggerEvent: "push"}
|
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
|
||||||
|
}
|
||||||
|
|
||||||
notify_service.RegisterNotifier(NewNotifier())
|
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) {
|
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) {
|
defer MockMailSettings(func(msgs ...*Message) {
|
||||||
assert.Fail(t, "no mail should be sent")
|
assert.Fail(t, "no mail should be sent")
|
||||||
})()
|
})()
|
||||||
notify_service.ActionRunNowDone(ctx, run2, actions_model.StatusRunning, nil)
|
notify_service.ActionRunNowDone(ctx, run2, actions_model.StatusRunning, nil)
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("SendNotificationEmailOnActionRunFailed", func(t *testing.T) {
|
t.Run("WorkflowEnableEmailNotificationIsFalse", func(t *testing.T) {
|
||||||
mailSentToOwner := false
|
user := getActionsNowDoneTestUser(t, "new_user1", "new_user1@example.com", "enabled")
|
||||||
mailSentToTriggerUser := false
|
defer CleanUpUsers(ctx, []*user_model.User{user})
|
||||||
|
assignUsers(user, user)
|
||||||
defer MockMailSettings(func(msgs ...*Message) {
|
defer MockMailSettings(func(msgs ...*Message) {
|
||||||
assert.LessOrEqual(t, len(msgs), 2)
|
assert.Fail(t, "no mail should be sent")
|
||||||
for _, msg := range msgs {
|
})()
|
||||||
switch msg.To {
|
run2.NotifyEmail = false
|
||||||
case triggerUser.EmailTo():
|
notify_service.ActionRunNowDone(ctx, run2, actions_model.StatusRunning, nil)
|
||||||
assert.False(t, mailSentToTriggerUser, "sent mail twice")
|
})
|
||||||
mailSentToTriggerUser = true
|
|
||||||
case ownerUser.EmailTo():
|
for _, testCase := range []struct {
|
||||||
assert.False(t, mailSentToOwner, "sent mail twice")
|
name string
|
||||||
mailSentToOwner = true
|
triggerUser *user_model.User
|
||||||
default:
|
owner *user_model.User
|
||||||
assert.Fail(t, "sent mail to unknown sender", msg.To)
|
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("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
|
||||||
}
|
}
|
||||||
assert.Contains(t, msg.Body, triggerUser.HTMLURL())
|
require.Contains(t, msg.To, expectedEmail, "sent mail to unknown sender")
|
||||||
assert.Contains(t, msg.Body, triggerUser.Name)
|
mailSent = true
|
||||||
|
assert.Contains(t, msg.Body, testCase.triggerUser.HTMLURL())
|
||||||
|
assert.Contains(t, msg.Body, testCase.triggerUser.Name)
|
||||||
// what happened
|
// what happened
|
||||||
assert.Contains(t, msg.Body, "failed")
|
assert.Contains(t, msg.Body, "failed")
|
||||||
// new status of run
|
// new status of run
|
||||||
|
@ -101,31 +200,27 @@ func TestActionRunNowDoneNotificationMail(t *testing.T) {
|
||||||
// prior status of this run
|
// prior status of this run
|
||||||
assert.Contains(t, msg.Body, "waiting")
|
assert.Contains(t, msg.Body, "waiting")
|
||||||
assertTranslatedLocaleMailActionsNowDone(t, msg.Body)
|
assertTranslatedLocaleMailActionsNowDone(t, msg.Body)
|
||||||
}
|
|
||||||
})()
|
})()
|
||||||
|
require.NotNil(t, setting.MailService)
|
||||||
|
|
||||||
notify_service.ActionRunNowDone(ctx, run1, actions_model.StatusWaiting, nil)
|
notify_service.ActionRunNowDone(ctx, run1, actions_model.StatusWaiting, nil)
|
||||||
assert.True(t, mailSentToOwner)
|
assert.Equal(t, testCase.expectMail, mailSent)
|
||||||
assert.True(t, mailSentToTriggerUser)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("SendNotificationEmailOnActionRunRecovered", func(t *testing.T) {
|
t.Run("SendNotificationEmailOnActionRunRecovered", func(t *testing.T) {
|
||||||
mailSentToOwner := false
|
mailSent := false
|
||||||
mailSentToTriggerUser := false
|
|
||||||
defer MockMailSettings(func(msgs ...*Message) {
|
defer MockMailSettings(func(msgs ...*Message) {
|
||||||
assert.LessOrEqual(t, len(msgs), 2)
|
assert.Len(t, msgs, 1)
|
||||||
for _, msg := range msgs {
|
msg := msgs[0]
|
||||||
switch msg.To {
|
assert.False(t, mailSent, "sent mail twice")
|
||||||
case triggerUser.EmailTo():
|
expectedEmail := testCase.triggerUser.Email
|
||||||
assert.False(t, mailSentToTriggerUser, "sent mail twice")
|
if testCase.expected == "owner" { // otherwise "trigger"
|
||||||
mailSentToTriggerUser = true
|
expectedEmail = testCase.owner.Email
|
||||||
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())
|
require.Contains(t, msg.To, expectedEmail, "sent mail to unknown sender")
|
||||||
assert.Contains(t, msg.Body, triggerUser.Name)
|
mailSent = true
|
||||||
|
assert.Contains(t, msg.Body, testCase.triggerUser.HTMLURL())
|
||||||
|
assert.Contains(t, msg.Body, testCase.triggerUser.Name)
|
||||||
// what happened
|
// what happened
|
||||||
assert.Contains(t, msg.Body, "recovered")
|
assert.Contains(t, msg.Body, "recovered")
|
||||||
// old status of run
|
// old status of run
|
||||||
|
@ -134,13 +229,12 @@ func TestActionRunNowDoneNotificationMail(t *testing.T) {
|
||||||
assert.Contains(t, msg.Body, "success")
|
assert.Contains(t, msg.Body, "success")
|
||||||
// prior status of this run
|
// prior status of this run
|
||||||
assert.Contains(t, msg.Body, "running")
|
assert.Contains(t, msg.Body, "running")
|
||||||
assertTranslatedLocaleMailActionsNowDone(t, msg.Body)
|
|
||||||
}
|
|
||||||
})()
|
})()
|
||||||
assert.NotNil(t, setting.MailService)
|
require.NotNil(t, setting.MailService)
|
||||||
|
|
||||||
notify_service.ActionRunNowDone(ctx, run2, actions_model.StatusRunning, run1)
|
notify_service.ActionRunNowDone(ctx, run2, actions_model.StatusRunning, run1)
|
||||||
assert.True(t, mailSentToOwner)
|
assert.Equal(t, testCase.expectMail, mailSent)
|
||||||
assert.True(t, mailSentToTriggerUser)
|
|
||||||
})
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,6 +8,7 @@ import (
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"forgejo.org/models/db"
|
"forgejo.org/models/db"
|
||||||
|
organization_model "forgejo.org/models/organization"
|
||||||
"forgejo.org/models/unittest"
|
"forgejo.org/models/unittest"
|
||||||
user_model "forgejo.org/models/user"
|
user_model "forgejo.org/models/user"
|
||||||
"forgejo.org/modules/setting"
|
"forgejo.org/modules/setting"
|
||||||
|
@ -51,6 +52,11 @@ func MockMailSettings(send func(msgs ...*Message)) func() {
|
||||||
|
|
||||||
func CleanUpUsers(ctx context.Context, users []*user_model.User) {
|
func CleanUpUsers(ctx context.Context, users []*user_model.User) {
|
||||||
for _, u := range users {
|
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.DeleteByID[user_model.User](ctx, u.ID)
|
||||||
|
db.DeleteByBean(ctx, &user_model.EmailAddress{UID: u.ID})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -894,9 +894,16 @@ func (m *webhookNotifier) ActionRunNowDone(ctx context.Context, run *actions_mod
|
||||||
Owner: run.TriggerUser,
|
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{
|
payload := &api.ActionPayload{
|
||||||
Run: convert.ToActionRun(ctx, run),
|
Run: convert.ToActionRun(ctx, run, doer),
|
||||||
LastRun: convert.ToActionRun(ctx, lastRun),
|
LastRun: convert.ToActionRun(ctx, lastRun, doer),
|
||||||
PriorStatus: priorStatus.String(),
|
PriorStatus: priorStatus.String(),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
{{template "base/alert"}}
|
{{template "base/alert"}}
|
||||||
{{range .Issue.Comments}}
|
{{range .Issue.Comments}}
|
||||||
{{if call $.ShouldShowCommentType .Type}}
|
{{if call $.ShouldShowCommentType .Type}}
|
||||||
{{$createdStr:= DateUtils.TimeSince .CreatedUnix}}
|
{{$createdStr := HTMLFormat `<a id="%s" href="#%s">%s</a>` .EventTag .HashTag (DateUtils.TimeSince .CreatedUnix)}}
|
||||||
|
|
||||||
<!-- 0 = COMMENT, 1 = REOPEN, 2 = CLOSE, 3 = ISSUE_REF, 4 = COMMIT_REF,
|
<!-- 0 = COMMENT, 1 = REOPEN, 2 = CLOSE, 3 = ISSUE_REF, 4 = COMMIT_REF,
|
||||||
5 = COMMENT_REF, 6 = PULL_REF, 7 = COMMENT_LABEL, 8 = MILESTONE_CHANGE,
|
5 = COMMENT_REF, 6 = PULL_REF, 7 = COMMENT_LABEL, 8 = MILESTONE_CHANGE,
|
||||||
|
@ -87,9 +87,9 @@
|
||||||
<span class="text grey muted-links">
|
<span class="text grey muted-links">
|
||||||
{{template "repo/issue/view_content/comments_authorlink" dict "ctxData" $ "comment" .}}
|
{{template "repo/issue/view_content/comments_authorlink" dict "ctxData" $ "comment" .}}
|
||||||
{{if .Issue.IsPull}}
|
{{if .Issue.IsPull}}
|
||||||
{{ctx.Locale.Tr "repo.pulls.reopened_at" .EventTag $createdStr}}
|
{{ctx.Locale.Tr "repo.pulls.reopened_at" $createdStr}}
|
||||||
{{else}}
|
{{else}}
|
||||||
{{ctx.Locale.Tr "repo.issues.reopened_at" .EventTag $createdStr}}
|
{{ctx.Locale.Tr "repo.issues.reopened_at" $createdStr}}
|
||||||
{{end}}
|
{{end}}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
@ -102,9 +102,9 @@
|
||||||
<span class="text grey muted-links">
|
<span class="text grey muted-links">
|
||||||
{{template "repo/issue/view_content/comments_authorlink" dict "ctxData" $ "comment" .}}
|
{{template "repo/issue/view_content/comments_authorlink" dict "ctxData" $ "comment" .}}
|
||||||
{{if .Issue.IsPull}}
|
{{if .Issue.IsPull}}
|
||||||
{{ctx.Locale.Tr "repo.pulls.closed_at" .EventTag $createdStr}}
|
{{ctx.Locale.Tr "repo.pulls.closed_at" $createdStr}}
|
||||||
{{else}}
|
{{else}}
|
||||||
{{ctx.Locale.Tr "repo.issues.closed_at" .EventTag $createdStr}}
|
{{ctx.Locale.Tr "repo.issues.closed_at" $createdStr}}
|
||||||
{{end}}
|
{{end}}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
@ -137,14 +137,13 @@
|
||||||
{{else if eq .RefAction 2}}
|
{{else if eq .RefAction 2}}
|
||||||
{{$refTr = "repo.issues.ref_reopening_from"}}
|
{{$refTr = "repo.issues.ref_reopening_from"}}
|
||||||
{{end}}
|
{{end}}
|
||||||
{{$createdStr:= DateUtils.TimeSince .CreatedUnix}}
|
|
||||||
<div class="timeline-item event" id="{{.HashTag}}">
|
<div class="timeline-item event" id="{{.HashTag}}">
|
||||||
<span class="badge">{{svg "octicon-bookmark"}}</span>
|
<span class="badge">{{svg "octicon-bookmark"}}</span>
|
||||||
{{template "shared/user/avatarlink" dict "user" .Poster}}
|
{{template "shared/user/avatarlink" dict "user" .Poster}}
|
||||||
{{if eq .RefAction 3}}<del>{{end}}
|
{{if eq .RefAction 3}}<del>{{end}}
|
||||||
<span class="text grey muted-links">
|
<span class="text grey muted-links">
|
||||||
{{template "shared/user/authorlink" .Poster}}
|
{{template "shared/user/authorlink" .Poster}}
|
||||||
{{ctx.Locale.Tr $refTr .EventTag $createdStr (.RefCommentLink ctx) $refFrom}}
|
{{ctx.Locale.Tr $refTr $createdStr (.RefCommentLink ctx) $refFrom}}
|
||||||
</span>
|
</span>
|
||||||
{{if eq .RefAction 3}}</del>{{end}}
|
{{if eq .RefAction 3}}</del>{{end}}
|
||||||
|
|
||||||
|
@ -159,9 +158,9 @@
|
||||||
<span class="text grey muted-links">
|
<span class="text grey muted-links">
|
||||||
{{template "shared/user/authorlink" .Poster}}
|
{{template "shared/user/authorlink" .Poster}}
|
||||||
{{if .Issue.IsPull}}
|
{{if .Issue.IsPull}}
|
||||||
{{ctx.Locale.Tr "repo.pulls.commit_ref_at" .EventTag $createdStr}}
|
{{ctx.Locale.Tr "repo.pulls.commit_ref_at" $createdStr}}
|
||||||
{{else}}
|
{{else}}
|
||||||
{{ctx.Locale.Tr "repo.issues.commit_ref_at" .EventTag $createdStr}}
|
{{ctx.Locale.Tr "repo.issues.commit_ref_at" $createdStr}}
|
||||||
{{end}}
|
{{end}}
|
||||||
</span>
|
</span>
|
||||||
<div class="detail flex-text-block">
|
<div class="detail flex-text-block">
|
||||||
|
|
|
@ -32,6 +32,12 @@
|
||||||
</audio>
|
</audio>
|
||||||
{{else if .IsPDFFile}}
|
{{else if .IsPDFFile}}
|
||||||
<div class="pdf-content is-loading" data-src="{{$.RawFileLink}}" data-fallback-button-text="{{ctx.Locale.Tr "repo.diff.view_file"}}"></div>
|
<div class="pdf-content is-loading" data-src="{{$.RawFileLink}}" data-fallback-button-text="{{ctx.Locale.Tr "repo.diff.view_file"}}"></div>
|
||||||
|
{{else if .Is3DModelFile}}
|
||||||
|
{{if .IsGLBFile}}
|
||||||
|
<model-viewer src="{{$.RawFileLink}}" ar shadow-intensity="2" camera-controls touch-action="pan-y"></model-viewer>
|
||||||
|
{{else}}
|
||||||
|
<a href="{{$.RawFileLink}}" rel="nofollow">{{ctx.Locale.Tr "repo.file_view_raw"}}!</a>
|
||||||
|
{{end}}
|
||||||
{{else}}
|
{{else}}
|
||||||
<a href="{{$.RawFileLink}}" rel="nofollow">{{ctx.Locale.Tr "repo.file_view_raw"}}</a>
|
<a href="{{$.RawFileLink}}" rel="nofollow">{{ctx.Locale.Tr "repo.file_view_raw"}}</a>
|
||||||
{{end}}
|
{{end}}
|
||||||
|
|
|
@ -116,6 +116,12 @@
|
||||||
</audio>
|
</audio>
|
||||||
{{else if .IsPDFFile}}
|
{{else if .IsPDFFile}}
|
||||||
<div class="pdf-content is-loading" data-src="{{$.RawFileLink}}" data-fallback-button-text="{{ctx.Locale.Tr "repo.diff.view_file"}}"></div>
|
<div class="pdf-content is-loading" data-src="{{$.RawFileLink}}" data-fallback-button-text="{{ctx.Locale.Tr "repo.diff.view_file"}}"></div>
|
||||||
|
{{else if .Is3DModelFile}}
|
||||||
|
{{if .IsGLBFile}}
|
||||||
|
<model-viewer src="{{$.RawFileLink}}" ar shadow-intensity="2" camera-controls touch-action="pan-y"></model-viewer>
|
||||||
|
{{else}}
|
||||||
|
<a href="{{$.RawFileLink}}" rel="nofollow">{{ctx.Locale.Tr "repo.file_view_raw"}}</a>
|
||||||
|
{{end}}
|
||||||
{{else}}
|
{{else}}
|
||||||
<a href="{{$.RawFileLink}}" rel="nofollow">{{ctx.Locale.Tr "repo.file_view_raw"}}</a>
|
<a href="{{$.RawFileLink}}" rel="nofollow">{{ctx.Locale.Tr "repo.file_view_raw"}}</a>
|
||||||
{{end}}
|
{{end}}
|
||||||
|
|
209
templates/swagger/v1_json.tmpl
generated
209
templates/swagger/v1_json.tmpl
generated
|
@ -4985,7 +4985,7 @@
|
||||||
],
|
],
|
||||||
"responses": {
|
"responses": {
|
||||||
"200": {
|
"200": {
|
||||||
"$ref": "#/responses/RepoActionRunList"
|
"$ref": "#/responses/ActionRunList"
|
||||||
},
|
},
|
||||||
"400": {
|
"400": {
|
||||||
"$ref": "#/responses/error"
|
"$ref": "#/responses/error"
|
||||||
|
@ -5032,7 +5032,7 @@
|
||||||
],
|
],
|
||||||
"responses": {
|
"responses": {
|
||||||
"200": {
|
"200": {
|
||||||
"$ref": "#/responses/RepoActionRun"
|
"$ref": "#/responses/ActionRun"
|
||||||
},
|
},
|
||||||
"400": {
|
"400": {
|
||||||
"$ref": "#/responses/error"
|
"$ref": "#/responses/error"
|
||||||
|
@ -21120,6 +21120,129 @@
|
||||||
},
|
},
|
||||||
"x-go-package": "forgejo.org/modules/structs"
|
"x-go-package": "forgejo.org/modules/structs"
|
||||||
},
|
},
|
||||||
|
"ActionRun": {
|
||||||
|
"description": "ActionRun represents an action run",
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"ScheduleID": {
|
||||||
|
"description": "the cron id for the schedule trigger",
|
||||||
|
"type": "integer",
|
||||||
|
"format": "int64"
|
||||||
|
},
|
||||||
|
"approved_by": {
|
||||||
|
"description": "who approved this action run",
|
||||||
|
"type": "integer",
|
||||||
|
"format": "int64",
|
||||||
|
"x-go-name": "ApprovedBy"
|
||||||
|
},
|
||||||
|
"commit_sha": {
|
||||||
|
"description": "the commit sha the action run ran on",
|
||||||
|
"type": "string",
|
||||||
|
"x-go-name": "CommitSHA"
|
||||||
|
},
|
||||||
|
"created": {
|
||||||
|
"description": "when the action run was created",
|
||||||
|
"type": "string",
|
||||||
|
"format": "date-time",
|
||||||
|
"x-go-name": "Created"
|
||||||
|
},
|
||||||
|
"duration": {
|
||||||
|
"$ref": "#/definitions/Duration"
|
||||||
|
},
|
||||||
|
"event": {
|
||||||
|
"description": "the webhook event that causes the workflow to run",
|
||||||
|
"type": "string",
|
||||||
|
"x-go-name": "Event"
|
||||||
|
},
|
||||||
|
"event_payload": {
|
||||||
|
"description": "the payload of the webhook event that causes the workflow to run",
|
||||||
|
"type": "string",
|
||||||
|
"x-go-name": "EventPayload"
|
||||||
|
},
|
||||||
|
"html_url": {
|
||||||
|
"description": "the url of this action run",
|
||||||
|
"type": "string",
|
||||||
|
"x-go-name": "HTMLURL"
|
||||||
|
},
|
||||||
|
"id": {
|
||||||
|
"description": "the action run id",
|
||||||
|
"type": "integer",
|
||||||
|
"format": "int64",
|
||||||
|
"x-go-name": "ID"
|
||||||
|
},
|
||||||
|
"index_in_repo": {
|
||||||
|
"description": "a unique number for each run of a repository",
|
||||||
|
"type": "integer",
|
||||||
|
"format": "int64",
|
||||||
|
"x-go-name": "Index"
|
||||||
|
},
|
||||||
|
"is_fork_pull_request": {
|
||||||
|
"description": "If this is triggered by a PR from a forked repository or an untrusted user, we need to check if it is approved and limit permissions when running the workflow.",
|
||||||
|
"type": "boolean",
|
||||||
|
"x-go-name": "IsForkPullRequest"
|
||||||
|
},
|
||||||
|
"is_ref_deleted": {
|
||||||
|
"description": "has the commit/tag/… the action run ran on been deleted",
|
||||||
|
"type": "boolean",
|
||||||
|
"x-go-name": "IsRefDeleted"
|
||||||
|
},
|
||||||
|
"need_approval": {
|
||||||
|
"description": "may need approval if it's a fork pull request",
|
||||||
|
"type": "boolean",
|
||||||
|
"x-go-name": "NeedApproval"
|
||||||
|
},
|
||||||
|
"prettyref": {
|
||||||
|
"description": "the commit/tag/… the action run ran on",
|
||||||
|
"type": "string",
|
||||||
|
"x-go-name": "PrettyRef"
|
||||||
|
},
|
||||||
|
"repository": {
|
||||||
|
"$ref": "#/definitions/Repository"
|
||||||
|
},
|
||||||
|
"started": {
|
||||||
|
"description": "when the action run was started",
|
||||||
|
"type": "string",
|
||||||
|
"format": "date-time",
|
||||||
|
"x-go-name": "Started"
|
||||||
|
},
|
||||||
|
"status": {
|
||||||
|
"description": "the current status of this run",
|
||||||
|
"type": "string",
|
||||||
|
"x-go-name": "Status"
|
||||||
|
},
|
||||||
|
"stopped": {
|
||||||
|
"description": "when the action run was stopped",
|
||||||
|
"type": "string",
|
||||||
|
"format": "date-time",
|
||||||
|
"x-go-name": "Stopped"
|
||||||
|
},
|
||||||
|
"title": {
|
||||||
|
"description": "the action run's title",
|
||||||
|
"type": "string",
|
||||||
|
"x-go-name": "Title"
|
||||||
|
},
|
||||||
|
"trigger_event": {
|
||||||
|
"description": "the trigger event defined in the `on` configuration of the triggered workflow",
|
||||||
|
"type": "string",
|
||||||
|
"x-go-name": "TriggerEvent"
|
||||||
|
},
|
||||||
|
"trigger_user": {
|
||||||
|
"$ref": "#/definitions/User"
|
||||||
|
},
|
||||||
|
"updated": {
|
||||||
|
"description": "when the action run was last updated",
|
||||||
|
"type": "string",
|
||||||
|
"format": "date-time",
|
||||||
|
"x-go-name": "Updated"
|
||||||
|
},
|
||||||
|
"workflow_id": {
|
||||||
|
"description": "the name of workflow file",
|
||||||
|
"type": "string",
|
||||||
|
"x-go-name": "WorkflowID"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"x-go-package": "forgejo.org/modules/structs"
|
||||||
|
},
|
||||||
"ActionRunJob": {
|
"ActionRunJob": {
|
||||||
"description": "ActionRunJob represents a job of a run",
|
"description": "ActionRunJob represents a job of a run",
|
||||||
"type": "object",
|
"type": "object",
|
||||||
|
@ -23610,6 +23733,12 @@
|
||||||
},
|
},
|
||||||
"x-go-package": "forgejo.org/modules/structs"
|
"x-go-package": "forgejo.org/modules/structs"
|
||||||
},
|
},
|
||||||
|
"Duration": {
|
||||||
|
"description": "A Duration represents the elapsed time between two instants\nas an int64 nanosecond count. The representation limits the\nlargest representable duration to approximately 290 years.",
|
||||||
|
"type": "integer",
|
||||||
|
"format": "int64",
|
||||||
|
"x-go-package": "time"
|
||||||
|
},
|
||||||
"EditAttachmentOptions": {
|
"EditAttachmentOptions": {
|
||||||
"description": "EditAttachmentOptions options for editing attachments",
|
"description": "EditAttachmentOptions options for editing attachments",
|
||||||
"type": "object",
|
"type": "object",
|
||||||
|
@ -25576,7 +25705,7 @@
|
||||||
},
|
},
|
||||||
"x-go-package": "forgejo.org/modules/structs"
|
"x-go-package": "forgejo.org/modules/structs"
|
||||||
},
|
},
|
||||||
"ListRepoActionRunResponse": {
|
"ListActionRunResponse": {
|
||||||
"description": "ListActionRunResponse return a list of ActionRun",
|
"description": "ListActionRunResponse return a list of ActionRun",
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
|
@ -25588,7 +25717,7 @@
|
||||||
"workflow_runs": {
|
"workflow_runs": {
|
||||||
"type": "array",
|
"type": "array",
|
||||||
"items": {
|
"items": {
|
||||||
"$ref": "#/definitions/RepoActionRun"
|
"$ref": "#/definitions/ActionRun"
|
||||||
},
|
},
|
||||||
"x-go-name": "Entries"
|
"x-go-name": "Entries"
|
||||||
}
|
}
|
||||||
|
@ -27353,54 +27482,6 @@
|
||||||
},
|
},
|
||||||
"x-go-package": "forgejo.org/modules/structs"
|
"x-go-package": "forgejo.org/modules/structs"
|
||||||
},
|
},
|
||||||
"RepoActionRun": {
|
|
||||||
"description": "ActionRun represents an ActionRun",
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"event": {
|
|
||||||
"type": "string",
|
|
||||||
"x-go-name": "Event"
|
|
||||||
},
|
|
||||||
"head_branch": {
|
|
||||||
"type": "string",
|
|
||||||
"x-go-name": "HeadBranch"
|
|
||||||
},
|
|
||||||
"head_sha": {
|
|
||||||
"type": "string",
|
|
||||||
"x-go-name": "HeadSHA"
|
|
||||||
},
|
|
||||||
"id": {
|
|
||||||
"type": "integer",
|
|
||||||
"format": "int64",
|
|
||||||
"x-go-name": "ID"
|
|
||||||
},
|
|
||||||
"name": {
|
|
||||||
"type": "string",
|
|
||||||
"x-go-name": "Name"
|
|
||||||
},
|
|
||||||
"run_number": {
|
|
||||||
"type": "integer",
|
|
||||||
"format": "int64",
|
|
||||||
"x-go-name": "RunNumber"
|
|
||||||
},
|
|
||||||
"status": {
|
|
||||||
"type": "string",
|
|
||||||
"x-go-name": "Status"
|
|
||||||
},
|
|
||||||
"triggering_actor": {
|
|
||||||
"$ref": "#/definitions/User"
|
|
||||||
},
|
|
||||||
"url": {
|
|
||||||
"type": "string",
|
|
||||||
"x-go-name": "URL"
|
|
||||||
},
|
|
||||||
"workflow_id": {
|
|
||||||
"type": "string",
|
|
||||||
"x-go-name": "WorkflowID"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"x-go-package": "forgejo.org/modules/structs"
|
|
||||||
},
|
|
||||||
"RepoCollaboratorPermission": {
|
"RepoCollaboratorPermission": {
|
||||||
"description": "RepoCollaboratorPermission to get repository permission for a collaborator",
|
"description": "RepoCollaboratorPermission to get repository permission for a collaborator",
|
||||||
"type": "object",
|
"type": "object",
|
||||||
|
@ -28847,6 +28928,18 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"ActionRun": {
|
||||||
|
"description": "ActionRun",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/ActionRun"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"ActionRunList": {
|
||||||
|
"description": "ActionRunList",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/ListActionRunResponse"
|
||||||
|
}
|
||||||
|
},
|
||||||
"ActionVariable": {
|
"ActionVariable": {
|
||||||
"description": "ActionVariable",
|
"description": "ActionVariable",
|
||||||
"schema": {
|
"schema": {
|
||||||
|
@ -29616,18 +29709,6 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"RepoActionRun": {
|
|
||||||
"description": "RepoActionRun",
|
|
||||||
"schema": {
|
|
||||||
"$ref": "#/definitions/RepoActionRun"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"RepoActionRunList": {
|
|
||||||
"description": "RepoActionRunList",
|
|
||||||
"schema": {
|
|
||||||
"$ref": "#/definitions/ListRepoActionRunResponse"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"RepoCollaboratorPermission": {
|
"RepoCollaboratorPermission": {
|
||||||
"description": "RepoCollaboratorPermission",
|
"description": "RepoCollaboratorPermission",
|
||||||
"schema": {
|
"schema": {
|
||||||
|
|
|
@ -16,10 +16,15 @@ import (
|
||||||
var (
|
var (
|
||||||
changesetFiles []string
|
changesetFiles []string
|
||||||
changesetAvailable bool
|
changesetAvailable bool
|
||||||
globalFullRun bool
|
globalFullRun = false
|
||||||
)
|
)
|
||||||
|
|
||||||
func initChangedFiles() {
|
func initChangedFiles() {
|
||||||
|
_, globalFullRun = os.LookupEnv("RUN_ALL")
|
||||||
|
if globalFullRun {
|
||||||
|
log.Info("Full run of all tests requested via RUN_ALL environment.")
|
||||||
|
return
|
||||||
|
}
|
||||||
var changes string
|
var changes string
|
||||||
changes, changesetAvailable = os.LookupEnv("CHANGED_FILES")
|
changes, changesetAvailable = os.LookupEnv("CHANGED_FILES")
|
||||||
// the output of the Action seems to actually contain \n and not a newline literal
|
// the output of the Action seems to actually contain \n and not a newline literal
|
||||||
|
@ -44,7 +49,7 @@ func initChangedFiles() {
|
||||||
for _, expr := range globalPatterns {
|
for _, expr := range globalPatterns {
|
||||||
fullRunPatterns = append(fullRunPatterns, glob.MustCompile(expr, '.', '/'))
|
fullRunPatterns = append(fullRunPatterns, glob.MustCompile(expr, '.', '/'))
|
||||||
}
|
}
|
||||||
globalFullRun = false
|
|
||||||
for _, changedFile := range changesetFiles {
|
for _, changedFile := range changesetFiles {
|
||||||
for _, pattern := range fullRunPatterns {
|
for _, pattern := range fullRunPatterns {
|
||||||
if pattern.Match(changedFile) {
|
if pattern.Match(changedFile) {
|
||||||
|
|
88
tests/integration/actions_notifications_test.go
Normal file
88
tests/integration/actions_notifications_test.go
Normal file
|
@ -0,0 +1,88 @@
|
||||||
|
// Copyright 2025 The Forgejo Authors. All rights reserved.
|
||||||
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
|
package integration
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"net/url"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
actions_model "forgejo.org/models/actions"
|
||||||
|
auth_model "forgejo.org/models/auth"
|
||||||
|
"forgejo.org/models/unittest"
|
||||||
|
user_model "forgejo.org/models/user"
|
||||||
|
"forgejo.org/modules/setting"
|
||||||
|
"forgejo.org/tests"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestActionNotifications(t *testing.T) {
|
||||||
|
if !setting.Database.Type.IsSQLite3() {
|
||||||
|
t.Skip()
|
||||||
|
}
|
||||||
|
|
||||||
|
testCases := []struct {
|
||||||
|
name string
|
||||||
|
treePath string
|
||||||
|
fileContent string
|
||||||
|
notifyEmail bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "enabled",
|
||||||
|
treePath: ".forgejo/workflows/enabled.yml",
|
||||||
|
fileContent: `name: enabled
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
enable-email-notifications: true
|
||||||
|
jobs:
|
||||||
|
job1:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- run: echo job1
|
||||||
|
`,
|
||||||
|
notifyEmail: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "disabled",
|
||||||
|
treePath: ".forgejo/workflows/disabled.yml",
|
||||||
|
fileContent: `name: disabled
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
jobs:
|
||||||
|
job1:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- run: echo job1
|
||||||
|
`,
|
||||||
|
notifyEmail: false,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
onGiteaRun(t, func(t *testing.T, u *url.URL) {
|
||||||
|
user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
|
||||||
|
session := loginUser(t, user2.Name)
|
||||||
|
token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository, auth_model.AccessTokenScopeWriteUser)
|
||||||
|
|
||||||
|
for _, testCase := range testCases {
|
||||||
|
t.Run(testCase.name, func(t *testing.T) {
|
||||||
|
defer tests.PrintCurrentTest(t)()
|
||||||
|
|
||||||
|
apiRepo := createActionsTestRepo(t, token, testCase.name, false)
|
||||||
|
runner := newMockRunner()
|
||||||
|
runner.registerAsRepoRunner(t, user2.Name, apiRepo.Name, "mock-runner", []string{"ubuntu-latest"})
|
||||||
|
opts := getWorkflowCreateFileOptions(user2, apiRepo.DefaultBranch, fmt.Sprintf("create %s", testCase.treePath), testCase.fileContent)
|
||||||
|
createWorkflowFile(t, token, user2.Name, apiRepo.Name, testCase.treePath, opts)
|
||||||
|
|
||||||
|
task := runner.fetchTask(t)
|
||||||
|
actionTask := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionTask{ID: task.Id})
|
||||||
|
actionRunJob := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRunJob{ID: actionTask.JobID})
|
||||||
|
actionRun := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRun{ID: actionRunJob.RunID})
|
||||||
|
assert.Equal(t, testCase.notifyEmail, actionRun.NotifyEmail)
|
||||||
|
|
||||||
|
httpContext := NewAPITestContext(t, user2.Name, apiRepo.Name, auth_model.AccessTokenScopeWriteRepository)
|
||||||
|
doAPIDeleteRepository(httpContext)(t)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
|
@ -44,30 +44,35 @@ func (m *mockNotifier) ActionRunNowDone(ctx context.Context, run *actions_model.
|
||||||
assert.Equal(m.t, actions_model.StatusSuccess, run.Status)
|
assert.Equal(m.t, actions_model.StatusSuccess, run.Status)
|
||||||
assert.Equal(m.t, actions_model.StatusRunning, priorStatus)
|
assert.Equal(m.t, actions_model.StatusRunning, priorStatus)
|
||||||
assert.Nil(m.t, lastRun)
|
assert.Nil(m.t, lastRun)
|
||||||
|
assert.True(m.t, run.NotifyEmail)
|
||||||
case 1:
|
case 1:
|
||||||
assert.Equal(m.t, m.runID, run.ID)
|
assert.Equal(m.t, m.runID, run.ID)
|
||||||
assert.Equal(m.t, actions_model.StatusFailure, run.Status)
|
assert.Equal(m.t, actions_model.StatusFailure, run.Status)
|
||||||
assert.Equal(m.t, actions_model.StatusRunning, priorStatus)
|
assert.Equal(m.t, actions_model.StatusRunning, priorStatus)
|
||||||
assert.Equal(m.t, m.lastRunID, lastRun.ID)
|
assert.Equal(m.t, m.lastRunID, lastRun.ID)
|
||||||
assert.Equal(m.t, actions_model.StatusSuccess, lastRun.Status)
|
assert.Equal(m.t, actions_model.StatusSuccess, lastRun.Status)
|
||||||
|
assert.True(m.t, run.NotifyEmail)
|
||||||
case 2:
|
case 2:
|
||||||
assert.Equal(m.t, m.runID, run.ID)
|
assert.Equal(m.t, m.runID, run.ID)
|
||||||
assert.Equal(m.t, actions_model.StatusCancelled, run.Status)
|
assert.Equal(m.t, actions_model.StatusCancelled, run.Status)
|
||||||
assert.Equal(m.t, actions_model.StatusRunning, priorStatus)
|
assert.Equal(m.t, actions_model.StatusRunning, priorStatus)
|
||||||
assert.Equal(m.t, m.lastRunID, lastRun.ID)
|
assert.Equal(m.t, m.lastRunID, lastRun.ID)
|
||||||
assert.Equal(m.t, actions_model.StatusFailure, lastRun.Status)
|
assert.Equal(m.t, actions_model.StatusFailure, lastRun.Status)
|
||||||
|
assert.True(m.t, run.NotifyEmail)
|
||||||
case 3:
|
case 3:
|
||||||
assert.Equal(m.t, m.runID, run.ID)
|
assert.Equal(m.t, m.runID, run.ID)
|
||||||
assert.Equal(m.t, actions_model.StatusSuccess, run.Status)
|
assert.Equal(m.t, actions_model.StatusSuccess, run.Status)
|
||||||
assert.Equal(m.t, actions_model.StatusRunning, priorStatus)
|
assert.Equal(m.t, actions_model.StatusRunning, priorStatus)
|
||||||
assert.Equal(m.t, m.lastRunID, lastRun.ID)
|
assert.Equal(m.t, m.lastRunID, lastRun.ID)
|
||||||
assert.Equal(m.t, actions_model.StatusCancelled, lastRun.Status)
|
assert.Equal(m.t, actions_model.StatusCancelled, lastRun.Status)
|
||||||
|
assert.True(m.t, run.NotifyEmail)
|
||||||
case 4:
|
case 4:
|
||||||
assert.Equal(m.t, m.runID, run.ID)
|
assert.Equal(m.t, m.runID, run.ID)
|
||||||
assert.Equal(m.t, actions_model.StatusSuccess, run.Status)
|
assert.Equal(m.t, actions_model.StatusSuccess, run.Status)
|
||||||
assert.Equal(m.t, actions_model.StatusRunning, priorStatus)
|
assert.Equal(m.t, actions_model.StatusRunning, priorStatus)
|
||||||
assert.Equal(m.t, m.lastRunID, lastRun.ID)
|
assert.Equal(m.t, m.lastRunID, lastRun.ID)
|
||||||
assert.Equal(m.t, actions_model.StatusSuccess, lastRun.Status)
|
assert.Equal(m.t, actions_model.StatusSuccess, lastRun.Status)
|
||||||
|
assert.True(m.t, run.NotifyEmail)
|
||||||
default:
|
default:
|
||||||
assert.Fail(m.t, "too many notifications")
|
assert.Fail(m.t, "too many notifications")
|
||||||
}
|
}
|
||||||
|
@ -101,6 +106,7 @@ func TestActionNowDoneNotification(t *testing.T) {
|
||||||
TreePath: ".forgejo/workflows/dispatch.yml",
|
TreePath: ".forgejo/workflows/dispatch.yml",
|
||||||
ContentReader: strings.NewReader(
|
ContentReader: strings.NewReader(
|
||||||
"name: test\n" +
|
"name: test\n" +
|
||||||
|
"enable-email-notifications: true\n" +
|
||||||
"on: [workflow_dispatch]\n" +
|
"on: [workflow_dispatch]\n" +
|
||||||
"jobs:\n" +
|
"jobs:\n" +
|
||||||
" test:\n" +
|
" test:\n" +
|
||||||
|
|
|
@ -169,7 +169,7 @@ func TestAPIGetListActionRun(t *testing.T) {
|
||||||
req.AddTokenAuth(token)
|
req.AddTokenAuth(token)
|
||||||
|
|
||||||
res := MakeRequest(t, req, http.StatusOK)
|
res := MakeRequest(t, req, http.StatusOK)
|
||||||
apiRuns := new(api.ListRepoActionRunResponse)
|
apiRuns := new(api.ListActionRunResponse)
|
||||||
DecodeJSON(t, res, apiRuns)
|
DecodeJSON(t, res, apiRuns)
|
||||||
|
|
||||||
assert.Equal(t, int64(len(tt.expectedIDs)), apiRuns.TotalCount)
|
assert.Equal(t, int64(len(tt.expectedIDs)), apiRuns.TotalCount)
|
||||||
|
@ -231,13 +231,13 @@ func TestAPIGetActionRun(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
dbRun := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRun{ID: tt.runID})
|
dbRun := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRun{ID: tt.runID})
|
||||||
apiRun := new(api.RepoActionRun)
|
apiRun := new(api.ActionRun)
|
||||||
DecodeJSON(t, res, apiRun)
|
DecodeJSON(t, res, apiRun)
|
||||||
|
|
||||||
assert.Equal(t, dbRun.Index, apiRun.RunNumber)
|
assert.Equal(t, dbRun.Index, apiRun.Index)
|
||||||
assert.Equal(t, dbRun.Status.String(), apiRun.Status)
|
assert.Equal(t, dbRun.Status.String(), apiRun.Status)
|
||||||
assert.Equal(t, dbRun.CommitSHA, apiRun.HeadSHA)
|
assert.Equal(t, dbRun.CommitSHA, apiRun.CommitSHA)
|
||||||
assert.Equal(t, dbRun.TriggerUserID, apiRun.TriggeringActor.ID)
|
assert.Equal(t, dbRun.TriggerUserID, apiRun.TriggerUser.ID)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
375
tests/integration/issue_comment_test.go
Normal file
375
tests/integration/issue_comment_test.go
Normal file
|
@ -0,0 +1,375 @@
|
||||||
|
// Copyright 2025 The Forgejo Authors. All rights reserved.
|
||||||
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
|
package integration
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"forgejo.org/tests"
|
||||||
|
|
||||||
|
"github.com/PuerkitoBio/goquery"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func testIssueCommentChangeEvent(t *testing.T, htmlDoc *HTMLDoc, commentID, badgeOcticon, avatarTitle, avatarLink string, texts, links []string) {
|
||||||
|
// Check badge octicon
|
||||||
|
badge := htmlDoc.Find("#issuecomment-" + commentID + " .badge svg." + badgeOcticon)
|
||||||
|
assert.Equal(t, 1, badge.Length())
|
||||||
|
|
||||||
|
// Check avatar title
|
||||||
|
avatarImg := htmlDoc.Find("#issuecomment-" + commentID + " img.avatar")
|
||||||
|
if len(avatarTitle) == 0 {
|
||||||
|
assert.Zero(t, avatarImg.Length())
|
||||||
|
} else {
|
||||||
|
assert.Equal(t, 1, avatarImg.Length())
|
||||||
|
title, exists := avatarImg.Attr("title")
|
||||||
|
assert.True(t, exists)
|
||||||
|
assert.Equal(t, avatarTitle, title)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check avatar link
|
||||||
|
avatarA := htmlDoc.Find("#issuecomment-" + commentID + " a.avatar")
|
||||||
|
if len(avatarLink) == 0 {
|
||||||
|
assert.Zero(t, avatarA.Length())
|
||||||
|
} else {
|
||||||
|
assert.Equal(t, 1, avatarA.Length())
|
||||||
|
href, exists := avatarA.Attr("href")
|
||||||
|
assert.True(t, exists)
|
||||||
|
assert.Equal(t, avatarLink, href)
|
||||||
|
}
|
||||||
|
|
||||||
|
event := htmlDoc.Find("#issuecomment-" + commentID + " .text")
|
||||||
|
|
||||||
|
// Check text content
|
||||||
|
for _, text := range texts {
|
||||||
|
assert.Contains(t, strings.Join(strings.Fields(event.Text()), " "), text)
|
||||||
|
}
|
||||||
|
|
||||||
|
var ids []string
|
||||||
|
var hrefs []string
|
||||||
|
event.Find("a").Each(func(i int, s *goquery.Selection) {
|
||||||
|
if id, exists := s.Attr("id"); exists {
|
||||||
|
ids = append(ids, id)
|
||||||
|
}
|
||||||
|
if href, exists := s.Attr("href"); exists {
|
||||||
|
hrefs = append(hrefs, href)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// Check anchors (id)
|
||||||
|
assert.Equal(t, []string{"event-" + commentID}, ids)
|
||||||
|
|
||||||
|
// Check links (href)
|
||||||
|
issueCommentLink := "#issuecomment-" + commentID
|
||||||
|
found := false
|
||||||
|
for _, link := range links {
|
||||||
|
if link == issueCommentLink {
|
||||||
|
found = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !found {
|
||||||
|
links = append(links, issueCommentLink)
|
||||||
|
}
|
||||||
|
assert.Equal(t, links, hrefs)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestIssueCommentChangeMilestone(t *testing.T) {
|
||||||
|
defer tests.PrepareTestEnv(t)()
|
||||||
|
|
||||||
|
req := NewRequest(t, "GET", "/user2/repo1/issues/1")
|
||||||
|
resp := MakeRequest(t, req, http.StatusOK)
|
||||||
|
htmlDoc := NewHTMLParser(t, resp.Body)
|
||||||
|
|
||||||
|
// Add milestone
|
||||||
|
testIssueCommentChangeEvent(t, htmlDoc, "2000",
|
||||||
|
"octicon-milestone", "User One", "/user1",
|
||||||
|
[]string{"user1 added this to the milestone1 milestone"},
|
||||||
|
[]string{"/user1"})
|
||||||
|
// []string{"/user1", "/user2/repo1/milestone/1"})
|
||||||
|
|
||||||
|
// Modify milestone
|
||||||
|
testIssueCommentChangeEvent(t, htmlDoc, "2001",
|
||||||
|
"octicon-milestone", "User One", "/user1",
|
||||||
|
[]string{"user1 modified the milestone from milestone1 to milestone2"},
|
||||||
|
[]string{"/user1"})
|
||||||
|
// []string{"/user1", "/user2/repo1/milestone/1", "/user2/repo1/milestone/2"})
|
||||||
|
|
||||||
|
// Remove milestone
|
||||||
|
testIssueCommentChangeEvent(t, htmlDoc, "2002",
|
||||||
|
"octicon-milestone", "User One", "/user1",
|
||||||
|
[]string{"user1 removed this from the milestone2 milestone"},
|
||||||
|
[]string{"/user1"})
|
||||||
|
// []string{"/user1", "/user2/repo1/milestone/2"})
|
||||||
|
|
||||||
|
// Deleted milestone
|
||||||
|
testIssueCommentChangeEvent(t, htmlDoc, "2003",
|
||||||
|
"octicon-milestone", "User One", "/user1",
|
||||||
|
[]string{"user1 added this to the (deleted) milestone"},
|
||||||
|
[]string{"/user1"})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestIssueCommentChangeProject(t *testing.T) {
|
||||||
|
defer tests.PrepareTestEnv(t)()
|
||||||
|
|
||||||
|
req := NewRequest(t, "GET", "/user2/repo1/issues/1")
|
||||||
|
resp := MakeRequest(t, req, http.StatusOK)
|
||||||
|
htmlDoc := NewHTMLParser(t, resp.Body)
|
||||||
|
|
||||||
|
// Add project
|
||||||
|
testIssueCommentChangeEvent(t, htmlDoc, "2010",
|
||||||
|
"octicon-project", "User One", "/user1",
|
||||||
|
[]string{"user1 added this to the First project project"},
|
||||||
|
[]string{"/user1"})
|
||||||
|
// []string{"/user1", "/user2/repo1/projects/1"})
|
||||||
|
|
||||||
|
// Modify project
|
||||||
|
testIssueCommentChangeEvent(t, htmlDoc, "2011",
|
||||||
|
"octicon-project", "User One", "/user1",
|
||||||
|
[]string{"user1 modified the project from First project to second project"},
|
||||||
|
[]string{"/user1"})
|
||||||
|
// []string{"/user1", "/user2/repo1/projects/1", "/user2/repo1/projects/2"})
|
||||||
|
|
||||||
|
// Remove project
|
||||||
|
testIssueCommentChangeEvent(t, htmlDoc, "2012",
|
||||||
|
"octicon-project", "User One", "/user1",
|
||||||
|
[]string{"user1 removed this from the second project project"},
|
||||||
|
[]string{"/user1"})
|
||||||
|
// []string{"/user1", "/user2/repo1/projects/2"})
|
||||||
|
|
||||||
|
// Deleted project
|
||||||
|
testIssueCommentChangeEvent(t, htmlDoc, "2013",
|
||||||
|
"octicon-project", "User One", "/user1",
|
||||||
|
[]string{"user1 added this to the (deleted) project"},
|
||||||
|
[]string{"/user1"})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestIssueCommentChangeLabel(t *testing.T) {
|
||||||
|
defer tests.PrepareTestEnv(t)()
|
||||||
|
|
||||||
|
req := NewRequest(t, "GET", "/user2/repo1/issues/1")
|
||||||
|
resp := MakeRequest(t, req, http.StatusOK)
|
||||||
|
htmlDoc := NewHTMLParser(t, resp.Body)
|
||||||
|
|
||||||
|
// Add multiple labels
|
||||||
|
testIssueCommentChangeEvent(t, htmlDoc, "2020",
|
||||||
|
"octicon-tag", "User One", "/user1",
|
||||||
|
[]string{"user1 added the label1 label2 labels "},
|
||||||
|
[]string{"/user1", "/user2/repo1/issues?labels=1", "/user2/repo1/issues?labels=2"})
|
||||||
|
assert.Empty(t, htmlDoc.Find("#issuecomment-2021 .text").Text())
|
||||||
|
|
||||||
|
// Remove single label
|
||||||
|
testIssueCommentChangeEvent(t, htmlDoc, "2022",
|
||||||
|
"octicon-tag", "< U<se>r Tw<o > ><", "/user2",
|
||||||
|
[]string{"user2 removed the label1 label "},
|
||||||
|
[]string{"/user2", "/user2/repo1/issues?labels=1"})
|
||||||
|
|
||||||
|
// Modify labels (add and remove)
|
||||||
|
testIssueCommentChangeEvent(t, htmlDoc, "2023",
|
||||||
|
"octicon-tag", "User One", "/user1",
|
||||||
|
[]string{"user1 added label1 and removed label2 labels "},
|
||||||
|
[]string{"/user1", "/user2/repo1/issues?labels=1", "/user2/repo1/issues?labels=2"})
|
||||||
|
assert.Empty(t, htmlDoc.Find("#issuecomment-2024 .text").Text())
|
||||||
|
|
||||||
|
// Add single label
|
||||||
|
testIssueCommentChangeEvent(t, htmlDoc, "2025",
|
||||||
|
"octicon-tag", "< U<se>r Tw<o > ><", "/user2",
|
||||||
|
[]string{"user2 added the label2 label "},
|
||||||
|
[]string{"/user2", "/user2/repo1/issues?labels=2"})
|
||||||
|
|
||||||
|
// Remove multiple labels
|
||||||
|
testIssueCommentChangeEvent(t, htmlDoc, "2026",
|
||||||
|
"octicon-tag", "User One", "/user1",
|
||||||
|
[]string{"user1 removed the label1 label2 labels "},
|
||||||
|
[]string{"/user1", "/user2/repo1/issues?labels=1", "/user2/repo1/issues?labels=2"})
|
||||||
|
assert.Empty(t, htmlDoc.Find("#issuecomment-2027 .text").Text())
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestIssueCommentChangeAssignee(t *testing.T) {
|
||||||
|
defer tests.PrepareTestEnv(t)()
|
||||||
|
|
||||||
|
req := NewRequest(t, "GET", "/user2/repo1/issues/1")
|
||||||
|
resp := MakeRequest(t, req, http.StatusOK)
|
||||||
|
htmlDoc := NewHTMLParser(t, resp.Body)
|
||||||
|
|
||||||
|
// Self-assign
|
||||||
|
testIssueCommentChangeEvent(t, htmlDoc, "2040",
|
||||||
|
"octicon-person", "User One", "/user1",
|
||||||
|
[]string{"user1 self-assigned this"},
|
||||||
|
[]string{"/user1"})
|
||||||
|
|
||||||
|
// Remove other
|
||||||
|
testIssueCommentChangeEvent(t, htmlDoc, "2041",
|
||||||
|
"octicon-person", "User One", "/user1",
|
||||||
|
[]string{"user1 was unassigned by user2"},
|
||||||
|
[]string{"/user1"})
|
||||||
|
// []string{"/user1", "/user2"})
|
||||||
|
|
||||||
|
// Add other
|
||||||
|
testIssueCommentChangeEvent(t, htmlDoc, "2042",
|
||||||
|
"octicon-person", "< U<se>r Tw<o > ><", "/user2",
|
||||||
|
[]string{"user2 was assigned by user1"},
|
||||||
|
[]string{"/user2"})
|
||||||
|
// []string{"/user2", "/user1"})
|
||||||
|
|
||||||
|
// Self-remove
|
||||||
|
testIssueCommentChangeEvent(t, htmlDoc, "2043",
|
||||||
|
"octicon-person", "< U<se>r Tw<o > ><", "/user2",
|
||||||
|
[]string{"user2 removed their assignment"},
|
||||||
|
[]string{"/user2"})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestIssueCommentChangeLock(t *testing.T) {
|
||||||
|
defer tests.PrepareTestEnv(t)()
|
||||||
|
|
||||||
|
req := NewRequest(t, "GET", "/user2/repo1/issues/1")
|
||||||
|
resp := MakeRequest(t, req, http.StatusOK)
|
||||||
|
htmlDoc := NewHTMLParser(t, resp.Body)
|
||||||
|
|
||||||
|
// Lock without reason
|
||||||
|
testIssueCommentChangeEvent(t, htmlDoc, "2050",
|
||||||
|
"octicon-lock", "User One", "/user1",
|
||||||
|
[]string{"user1 locked and limited conversation to collaborators"},
|
||||||
|
[]string{"/user1"})
|
||||||
|
|
||||||
|
// Unlock
|
||||||
|
testIssueCommentChangeEvent(t, htmlDoc, "2051",
|
||||||
|
"octicon-key", "User One", "/user1",
|
||||||
|
[]string{"user1 unlocked this conversation"},
|
||||||
|
[]string{"/user1"})
|
||||||
|
|
||||||
|
// Lock with reason
|
||||||
|
testIssueCommentChangeEvent(t, htmlDoc, "2052",
|
||||||
|
"octicon-lock", "User One", "/user1",
|
||||||
|
[]string{"user1 locked as Too heated and limited conversation to collaborators"},
|
||||||
|
[]string{"/user1"})
|
||||||
|
|
||||||
|
// Unlock
|
||||||
|
testIssueCommentChangeEvent(t, htmlDoc, "2053",
|
||||||
|
"octicon-key", "User One", "/user1",
|
||||||
|
[]string{"user1 unlocked this conversation"},
|
||||||
|
[]string{"/user1"})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestIssueCommentChangePin(t *testing.T) {
|
||||||
|
defer tests.PrepareTestEnv(t)()
|
||||||
|
|
||||||
|
req := NewRequest(t, "GET", "/user2/repo1/issues/1")
|
||||||
|
resp := MakeRequest(t, req, http.StatusOK)
|
||||||
|
htmlDoc := NewHTMLParser(t, resp.Body)
|
||||||
|
|
||||||
|
// Pin
|
||||||
|
testIssueCommentChangeEvent(t, htmlDoc, "2060",
|
||||||
|
"octicon-pin", "User One", "/user1",
|
||||||
|
[]string{"user1 pinned this"},
|
||||||
|
[]string{"/user1"})
|
||||||
|
|
||||||
|
// Unpin
|
||||||
|
testIssueCommentChangeEvent(t, htmlDoc, "2061",
|
||||||
|
"octicon-pin", "User One", "/user1",
|
||||||
|
[]string{"user1 unpinned this"},
|
||||||
|
[]string{"/user1"})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestIssueCommentChangeOpen(t *testing.T) {
|
||||||
|
defer tests.PrepareTestEnv(t)()
|
||||||
|
|
||||||
|
req := NewRequest(t, "GET", "/user2/repo1/issues/1")
|
||||||
|
resp := MakeRequest(t, req, http.StatusOK)
|
||||||
|
htmlDoc := NewHTMLParser(t, resp.Body)
|
||||||
|
|
||||||
|
// Close issue
|
||||||
|
testIssueCommentChangeEvent(t, htmlDoc, "2070",
|
||||||
|
"octicon-circle-slash", "User One", "/user1",
|
||||||
|
[]string{"user1 closed this issue"},
|
||||||
|
[]string{"/user1"})
|
||||||
|
|
||||||
|
// Reopen issue
|
||||||
|
testIssueCommentChangeEvent(t, htmlDoc, "2071",
|
||||||
|
"octicon-dot-fill", "< U<se>r Tw<o > ><", "/user2",
|
||||||
|
[]string{"user2 reopened this issue"},
|
||||||
|
[]string{"/user2"})
|
||||||
|
|
||||||
|
req = NewRequest(t, "GET", "/user2/repo1/pulls/2")
|
||||||
|
resp = MakeRequest(t, req, http.StatusOK)
|
||||||
|
htmlDoc = NewHTMLParser(t, resp.Body)
|
||||||
|
|
||||||
|
// Close pull request
|
||||||
|
testIssueCommentChangeEvent(t, htmlDoc, "2072",
|
||||||
|
"octicon-circle-slash", "User One", "/user1",
|
||||||
|
[]string{"user1 closed this pull request"},
|
||||||
|
[]string{"/user1"})
|
||||||
|
|
||||||
|
// Reopen pull request
|
||||||
|
testIssueCommentChangeEvent(t, htmlDoc, "2073",
|
||||||
|
"octicon-dot-fill", "< U<se>r Tw<o > ><", "/user2",
|
||||||
|
[]string{"user2 reopened this pull request"},
|
||||||
|
[]string{"/user2"})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestIssueCommentChangeIssueReference(t *testing.T) {
|
||||||
|
defer tests.PrepareTestEnv(t)()
|
||||||
|
|
||||||
|
req := NewRequest(t, "GET", "/user2/repo1/issues/1")
|
||||||
|
resp := MakeRequest(t, req, http.StatusOK)
|
||||||
|
htmlDoc := NewHTMLParser(t, resp.Body)
|
||||||
|
|
||||||
|
// Issue reference from issue
|
||||||
|
testIssueCommentChangeEvent(t, htmlDoc, "2080",
|
||||||
|
"octicon-bookmark", "User One", "/user1",
|
||||||
|
[]string{"user1 referenced this issue ", "issue5 #4"},
|
||||||
|
[]string{"/user1", "/user2/repo1/issues/4", "#issuecomment-2080", "/user2/repo1/issues/4"})
|
||||||
|
|
||||||
|
// Issue reference from pull
|
||||||
|
testIssueCommentChangeEvent(t, htmlDoc, "2081",
|
||||||
|
"octicon-bookmark", "User One", "/user1",
|
||||||
|
[]string{"user1 referenced this issue ", "issue2 #2"},
|
||||||
|
[]string{"/user1", "/user2/repo1/pulls/2", "#issuecomment-2081", "/user2/repo1/pulls/2"})
|
||||||
|
|
||||||
|
// Issue reference from issue in different repo
|
||||||
|
testIssueCommentChangeEvent(t, htmlDoc, "2082",
|
||||||
|
"octicon-bookmark", "User One", "/user1",
|
||||||
|
[]string{"user1 referenced this issue from org3/repo21", "just a normal issue #1"},
|
||||||
|
[]string{"/user1", "/org3/repo21/issues/1", "#issuecomment-2082", "/org3/repo21/issues/1"})
|
||||||
|
|
||||||
|
// Issue reference from pull in different repo
|
||||||
|
testIssueCommentChangeEvent(t, htmlDoc, "2083",
|
||||||
|
"octicon-bookmark", "User One", "/user1",
|
||||||
|
[]string{"user1 referenced this issue from user12/repo10 ", "pr2 #1"},
|
||||||
|
[]string{"/user1", "/user12/repo10/pulls/1", "#issuecomment-2083", "/user12/repo10/pulls/1"})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestIssueCommentChangePullReference(t *testing.T) {
|
||||||
|
defer tests.PrepareTestEnv(t)()
|
||||||
|
|
||||||
|
req := NewRequest(t, "GET", "/user2/repo1/pulls/2")
|
||||||
|
resp := MakeRequest(t, req, http.StatusOK)
|
||||||
|
htmlDoc := NewHTMLParser(t, resp.Body)
|
||||||
|
|
||||||
|
// Pull reference from issue
|
||||||
|
testIssueCommentChangeEvent(t, htmlDoc, "2090",
|
||||||
|
"octicon-bookmark", "User One", "/user1",
|
||||||
|
[]string{"user1 referenced this pull request ", "issue1 #1"},
|
||||||
|
[]string{"/user1", "/user2/repo1/issues/1", "#issuecomment-2090", "/user2/repo1/issues/1"})
|
||||||
|
|
||||||
|
// Pull reference from pull
|
||||||
|
testIssueCommentChangeEvent(t, htmlDoc, "2091",
|
||||||
|
"octicon-bookmark", "User One", "/user1",
|
||||||
|
[]string{"user1 referenced this pull request ", "issue2 #2"},
|
||||||
|
[]string{"/user1", "/user2/repo1/pulls/2", "#issuecomment-2091", "/user2/repo1/pulls/2"})
|
||||||
|
|
||||||
|
// Pull reference from issue in different repo
|
||||||
|
testIssueCommentChangeEvent(t, htmlDoc, "2092",
|
||||||
|
"octicon-bookmark", "User One", "/user1",
|
||||||
|
[]string{"user1 referenced this pull request from org3/repo21", "just a normal issue #1"},
|
||||||
|
[]string{"/user1", "/org3/repo21/issues/1", "#issuecomment-2092", "/org3/repo21/issues/1"})
|
||||||
|
|
||||||
|
// Pull reference from pull in different repo
|
||||||
|
testIssueCommentChangeEvent(t, htmlDoc, "2093",
|
||||||
|
"octicon-bookmark", "User One", "/user1",
|
||||||
|
[]string{"user1 referenced this pull request from user12/repo10 ", "pr2 #1"},
|
||||||
|
[]string{"/user1", "/user12/repo10/pulls/1", "#issuecomment-2093", "/user12/repo10/pulls/1"})
|
||||||
|
}
|
108
tests/integration/org_profile_test.go
Normal file
108
tests/integration/org_profile_test.go
Normal file
|
@ -0,0 +1,108 @@
|
||||||
|
// Copyright 2024 The Forgejo Authors c/o Codeberg e.V.. All rights reserved.
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
package integration
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"forgejo.org/models/unittest"
|
||||||
|
user_model "forgejo.org/models/user"
|
||||||
|
"forgejo.org/modules/setting"
|
||||||
|
"forgejo.org/modules/test"
|
||||||
|
files_service "forgejo.org/services/repository/files"
|
||||||
|
"forgejo.org/tests"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestOrgProfile(t *testing.T) {
|
||||||
|
onGiteaRun(t, func(t *testing.T, u *url.URL) {
|
||||||
|
checkReadme := func(t *testing.T, title, readmeFilename string, expectedCount int) {
|
||||||
|
t.Run(title, func(t *testing.T) {
|
||||||
|
defer tests.PrintCurrentTest(t)()
|
||||||
|
|
||||||
|
// Prepare the test repository
|
||||||
|
org3 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 3})
|
||||||
|
|
||||||
|
var ops []*files_service.ChangeRepoFile
|
||||||
|
op := "create"
|
||||||
|
if readmeFilename != "README.md" {
|
||||||
|
ops = append(ops, &files_service.ChangeRepoFile{
|
||||||
|
Operation: "delete",
|
||||||
|
TreePath: "README.md",
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
op = "update"
|
||||||
|
}
|
||||||
|
if readmeFilename != "" {
|
||||||
|
ops = append(ops, &files_service.ChangeRepoFile{
|
||||||
|
Operation: op,
|
||||||
|
TreePath: readmeFilename,
|
||||||
|
ContentReader: strings.NewReader("# Hi!\n"),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
_, _, f := tests.CreateDeclarativeRepo(t, org3, ".profile", nil, nil, ops)
|
||||||
|
defer f()
|
||||||
|
|
||||||
|
// Perform the test
|
||||||
|
req := NewRequest(t, "GET", "/org3")
|
||||||
|
resp := MakeRequest(t, req, http.StatusOK)
|
||||||
|
|
||||||
|
doc := NewHTMLParser(t, resp.Body)
|
||||||
|
readmeCount := doc.Find("#readme_profile").Length()
|
||||||
|
|
||||||
|
assert.Equal(t, expectedCount, readmeCount)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
checkReadme(t, "No readme", "", 0)
|
||||||
|
checkReadme(t, "README.md", "README.md", 1)
|
||||||
|
checkReadme(t, "readme.md", "readme.md", 1)
|
||||||
|
checkReadme(t, "ReadMe.mD", "ReadMe.mD", 1)
|
||||||
|
checkReadme(t, "readme.org does not render", "README.org", 0)
|
||||||
|
|
||||||
|
t.Run("readme-size", func(t *testing.T) {
|
||||||
|
defer tests.PrintCurrentTest(t)()
|
||||||
|
|
||||||
|
// Prepare the test repository
|
||||||
|
org3 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 3})
|
||||||
|
|
||||||
|
_, _, f := tests.CreateDeclarativeRepo(t, org3, ".profile", nil, nil, []*files_service.ChangeRepoFile{
|
||||||
|
{
|
||||||
|
Operation: "update",
|
||||||
|
TreePath: "README.md",
|
||||||
|
ContentReader: strings.NewReader(`## Lorem ipsum
|
||||||
|
dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.
|
||||||
|
## Ut enim ad minim veniam
|
||||||
|
quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum`),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
defer f()
|
||||||
|
|
||||||
|
t.Run("full", func(t *testing.T) {
|
||||||
|
defer tests.PrintCurrentTest(t)()
|
||||||
|
defer test.MockVariableValue(&setting.UI.MaxDisplayFileSize, 500)()
|
||||||
|
|
||||||
|
req := NewRequest(t, "GET", "/org3")
|
||||||
|
resp := MakeRequest(t, req, http.StatusOK)
|
||||||
|
assert.Contains(t, resp.Body.String(), "Ut enim ad minim veniam")
|
||||||
|
assert.Contains(t, resp.Body.String(), "mollit anim id est laborum")
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("truncated", func(t *testing.T) {
|
||||||
|
defer tests.PrintCurrentTest(t)()
|
||||||
|
defer test.MockVariableValue(&setting.UI.MaxDisplayFileSize, 146)()
|
||||||
|
|
||||||
|
req := NewRequest(t, "GET", "/org3")
|
||||||
|
resp := MakeRequest(t, req, http.StatusOK)
|
||||||
|
assert.Contains(t, resp.Body.String(), "Ut enim ad minim")
|
||||||
|
assert.NotContains(t, resp.Body.String(), "veniam")
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
|
@ -6,6 +6,8 @@ package integration
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
auth_model "forgejo.org/models/auth"
|
auth_model "forgejo.org/models/auth"
|
||||||
|
@ -113,3 +115,104 @@ func TestWikiTOC(t *testing.T) {
|
||||||
assert.Equal(t, "Helpdesk", htmlDoc.Find(".wiki-content-toc a").Text())
|
assert.Equal(t, "Helpdesk", htmlDoc.Find(".wiki-content-toc a").Text())
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func canEditWiki(t *testing.T, username, url string, canEdit bool) {
|
||||||
|
t.Helper()
|
||||||
|
// t.Parallel()
|
||||||
|
|
||||||
|
req := NewRequest(t, "GET", url)
|
||||||
|
|
||||||
|
var resp *httptest.ResponseRecorder
|
||||||
|
if username != "" {
|
||||||
|
session := loginUser(t, username)
|
||||||
|
resp = session.MakeRequest(t, req, http.StatusOK)
|
||||||
|
} else {
|
||||||
|
resp = MakeRequest(t, req, http.StatusOK)
|
||||||
|
}
|
||||||
|
doc := NewHTMLParser(t, resp.Body)
|
||||||
|
res := doc.Find(`a[href^="` + url + `"]`).Map(func(_ int, el *goquery.Selection) string {
|
||||||
|
return el.AttrOr("href", "")
|
||||||
|
})
|
||||||
|
found := false
|
||||||
|
for _, href := range res {
|
||||||
|
if strings.HasSuffix(href, "?action=_new") {
|
||||||
|
if !canEdit {
|
||||||
|
t.Errorf("unexpected edit link: %s", href)
|
||||||
|
}
|
||||||
|
found = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if canEdit {
|
||||||
|
assert.True(t, found, "could not find ?action=_new link among %v", res)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestWikiPermissions(t *testing.T) {
|
||||||
|
defer tests.PrepareTestEnv(t)()
|
||||||
|
|
||||||
|
t.Run("default settings", func(t *testing.T) {
|
||||||
|
t.Run("anonymous", func(t *testing.T) {
|
||||||
|
canEditWiki(t, "", "/user5/repo4/wiki", false)
|
||||||
|
})
|
||||||
|
t.Run("owner", func(t *testing.T) {
|
||||||
|
canEditWiki(t, "user5", "/user5/repo4/wiki", true)
|
||||||
|
})
|
||||||
|
t.Run("collaborator", func(t *testing.T) {
|
||||||
|
canEditWiki(t, "user4", "/user5/repo4/wiki", true)
|
||||||
|
canEditWiki(t, "user29", "/user5/repo4/wiki", true)
|
||||||
|
})
|
||||||
|
t.Run("other user", func(t *testing.T) {
|
||||||
|
canEditWiki(t, "user2", "/user5/repo4/wiki", false)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("saved unchanged settings", func(t *testing.T) {
|
||||||
|
session := loginUser(t, "user5")
|
||||||
|
csrf := GetCSRF(t, session, "/user5/repo4/settings/units")
|
||||||
|
req := NewRequestWithValues(t, "POST", "/user5/repo4/settings/units", map[string]string{
|
||||||
|
"_csrf": csrf,
|
||||||
|
"enable_wiki": "on",
|
||||||
|
})
|
||||||
|
session.MakeRequest(t, req, http.StatusSeeOther)
|
||||||
|
|
||||||
|
t.Run("anonymous", func(t *testing.T) {
|
||||||
|
canEditWiki(t, "", "/user5/repo4/wiki", false)
|
||||||
|
})
|
||||||
|
t.Run("owner", func(t *testing.T) {
|
||||||
|
canEditWiki(t, "user5", "/user5/repo4/wiki", true)
|
||||||
|
})
|
||||||
|
t.Run("collaborator", func(t *testing.T) {
|
||||||
|
canEditWiki(t, "user4", "/user5/repo4/wiki", true)
|
||||||
|
canEditWiki(t, "user29", "/user5/repo4/wiki", true)
|
||||||
|
})
|
||||||
|
t.Run("other user", func(t *testing.T) {
|
||||||
|
canEditWiki(t, "user2", "/user5/repo4/wiki", false)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("globally writable", func(t *testing.T) {
|
||||||
|
session := loginUser(t, "user5")
|
||||||
|
csrf := GetCSRF(t, session, "/user5/repo4/settings/units")
|
||||||
|
req := NewRequestWithValues(t, "POST", "/user5/repo4/settings/units", map[string]string{
|
||||||
|
"_csrf": csrf,
|
||||||
|
"enable_wiki": "on",
|
||||||
|
"globally_writeable_wiki": "on",
|
||||||
|
})
|
||||||
|
session.MakeRequest(t, req, http.StatusSeeOther)
|
||||||
|
|
||||||
|
t.Run("anonymous", func(t *testing.T) {
|
||||||
|
canEditWiki(t, "", "/user5/repo4/wiki", false)
|
||||||
|
})
|
||||||
|
t.Run("owner", func(t *testing.T) {
|
||||||
|
canEditWiki(t, "user5", "/user5/repo4/wiki", true)
|
||||||
|
})
|
||||||
|
t.Run("collaborator", func(t *testing.T) {
|
||||||
|
canEditWiki(t, "user4", "/user5/repo4/wiki", true)
|
||||||
|
canEditWiki(t, "user29", "/user5/repo4/wiki", true)
|
||||||
|
})
|
||||||
|
t.Run("other user", func(t *testing.T) {
|
||||||
|
canEditWiki(t, "user2", "/user5/repo4/wiki", true)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
|
@ -11,6 +11,8 @@ import (
|
||||||
|
|
||||||
"forgejo.org/models/unittest"
|
"forgejo.org/models/unittest"
|
||||||
user_model "forgejo.org/models/user"
|
user_model "forgejo.org/models/user"
|
||||||
|
"forgejo.org/modules/setting"
|
||||||
|
"forgejo.org/modules/test"
|
||||||
files_service "forgejo.org/services/repository/files"
|
files_service "forgejo.org/services/repository/files"
|
||||||
"forgejo.org/tests"
|
"forgejo.org/tests"
|
||||||
|
|
||||||
|
@ -63,5 +65,44 @@ func TestUserProfile(t *testing.T) {
|
||||||
checkReadme(t, "readme.md", "readme.md", 1)
|
checkReadme(t, "readme.md", "readme.md", 1)
|
||||||
checkReadme(t, "ReadMe.mD", "ReadMe.mD", 1)
|
checkReadme(t, "ReadMe.mD", "ReadMe.mD", 1)
|
||||||
checkReadme(t, "readme.org does not render", "README.org", 0)
|
checkReadme(t, "readme.org does not render", "README.org", 0)
|
||||||
|
|
||||||
|
t.Run("readme-size", func(t *testing.T) {
|
||||||
|
defer tests.PrintCurrentTest(t)()
|
||||||
|
|
||||||
|
// Prepare the test repository
|
||||||
|
user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
|
||||||
|
|
||||||
|
_, _, f := tests.CreateDeclarativeRepo(t, user2, ".profile", nil, nil, []*files_service.ChangeRepoFile{
|
||||||
|
{
|
||||||
|
Operation: "update",
|
||||||
|
TreePath: "README.md",
|
||||||
|
ContentReader: strings.NewReader(`## Lorem ipsum
|
||||||
|
dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.
|
||||||
|
## Ut enim ad minim veniam
|
||||||
|
quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum`),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
defer f()
|
||||||
|
|
||||||
|
t.Run("full", func(t *testing.T) {
|
||||||
|
defer tests.PrintCurrentTest(t)()
|
||||||
|
defer test.MockVariableValue(&setting.UI.MaxDisplayFileSize, 500)()
|
||||||
|
|
||||||
|
req := NewRequest(t, "GET", "/user2")
|
||||||
|
resp := MakeRequest(t, req, http.StatusOK)
|
||||||
|
assert.Contains(t, resp.Body.String(), "Ut enim ad minim veniam")
|
||||||
|
assert.Contains(t, resp.Body.String(), "mollit anim id est laborum")
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("truncated", func(t *testing.T) {
|
||||||
|
defer tests.PrintCurrentTest(t)()
|
||||||
|
defer test.MockVariableValue(&setting.UI.MaxDisplayFileSize, 146)()
|
||||||
|
|
||||||
|
req := NewRequest(t, "GET", "/user2")
|
||||||
|
resp := MakeRequest(t, req, http.StatusOK)
|
||||||
|
assert.Contains(t, resp.Body.String(), "Ut enim ad minim")
|
||||||
|
assert.NotContains(t, resp.Body.String(), "veniam")
|
||||||
|
})
|
||||||
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
@ -143,7 +143,7 @@ func TestUserRedirect(t *testing.T) {
|
||||||
defer test.MockVariableValue(&setting.Service.UsernameCooldownPeriod, 8)()
|
defer test.MockVariableValue(&setting.Service.UsernameCooldownPeriod, 8)()
|
||||||
defer tests.PrintCurrentTest(t)()
|
defer tests.PrintCurrentTest(t)()
|
||||||
|
|
||||||
assert.Contains(t, getPrompt(t), "The old username will be available to everyone after a cooldown period of 8 days, you can still reclaim the old username during the cooldown period.")
|
assert.Contains(t, getPrompt(t), "The old username will be available to everyone after a cooldown period of 8 days. You can still reclaim the old username during the cooldown period.")
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -167,7 +167,7 @@ func TestUserRedirect(t *testing.T) {
|
||||||
defer test.MockVariableValue(&setting.Service.UsernameCooldownPeriod, 8)()
|
defer test.MockVariableValue(&setting.Service.UsernameCooldownPeriod, 8)()
|
||||||
defer tests.PrintCurrentTest(t)()
|
defer tests.PrintCurrentTest(t)()
|
||||||
|
|
||||||
assert.Contains(t, getPrompt(t), "The old organization name will be available to everyone after a cooldown period of 8 days, you can still reclaim the old name during the cooldown period.")
|
assert.Contains(t, getPrompt(t), "The old organization name will be available to everyone after a cooldown period of 8 days. You can still reclaim the old name during the cooldown period.")
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
14
tests/testdata/data/viewer/README.md
vendored
Normal file
14
tests/testdata/data/viewer/README.md
vendored
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
# GLTF 3D Model Viewer
|
||||||
|
|
||||||
|
⚠️ Currently supports `.glb` files only.
|
||||||
|
|
||||||
|
3D models with the `.glb` format are rendered in repository file view.
|
||||||
|
|
||||||
|
## 🔨 How to Test
|
||||||
|
|
||||||
|
1) Create a new repository or use an existing one.
|
||||||
|
2) Upload a `.glb` file such as [Unicode❤♻Test.glb](./Unicode❤♻Test.glb) (CC0 1.0 Universal).
|
||||||
|
3) View the file in the repository.
|
||||||
|
- Similar to image files, the 3D model should be rendered in a viewer.
|
||||||
|
- Use mouse clicks to turn and zoom.
|
||||||
|
|
BIN
tests/testdata/data/viewer/Unicode❤♻Test.glb
vendored
Normal file
BIN
tests/testdata/data/viewer/Unicode❤♻Test.glb
vendored
Normal file
Binary file not shown.
|
@ -415,6 +415,11 @@ td .commit-summary {
|
||||||
max-width: 600px !important;
|
max-width: 600px !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
model-viewer {
|
||||||
|
width: 100%;
|
||||||
|
height: 100vh;
|
||||||
|
}
|
||||||
|
|
||||||
.pdf-content {
|
.pdf-content {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100vh;
|
height: 100vh;
|
||||||
|
|
|
@ -121,12 +121,12 @@ class ComboMarkdownEditor {
|
||||||
// Prevent special keyboard handling if currently a text expander popup is open
|
// Prevent special keyboard handling if currently a text expander popup is open
|
||||||
if (this.textarea.hasAttribute('aria-expanded')) return;
|
if (this.textarea.hasAttribute('aria-expanded')) return;
|
||||||
|
|
||||||
const noModifiers = !e.shiftKey && !e.ctrlKey && !e.altKey;
|
const noModifiers = !e.shiftKey && !e.ctrlKey && !e.altKey && !e.metaKey;
|
||||||
if (e.key === 'Escape') {
|
if (e.key === 'Escape') {
|
||||||
// Explicitly lose focus and reenable tab navigation.
|
// Explicitly lose focus and reenable tab navigation.
|
||||||
e.target.blur();
|
e.target.blur();
|
||||||
this.tabEnabled = false;
|
this.tabEnabled = false;
|
||||||
} else if (e.key === 'Tab' && this.tabEnabled && !e.altKey && !e.ctrlKey) {
|
} else if (e.key === 'Tab' && this.tabEnabled && !e.altKey && !e.ctrlKey && !e.metaKey) {
|
||||||
if (this.indentSelection(e.shiftKey, true)) {
|
if (this.indentSelection(e.shiftKey, true)) {
|
||||||
this.options?.onContentChanged?.(this, e);
|
this.options?.onContentChanged?.(this, e);
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
|
@ -23,6 +23,7 @@ import {initStopwatch} from './features/stopwatch.js';
|
||||||
import {initFindFileInRepo} from './features/repo-findfile.js';
|
import {initFindFileInRepo} from './features/repo-findfile.js';
|
||||||
import {initCommentContent, initMarkupContent} from './markup/content.js';
|
import {initCommentContent, initMarkupContent} from './markup/content.js';
|
||||||
import {initPdfViewer} from './render/pdf.js';
|
import {initPdfViewer} from './render/pdf.js';
|
||||||
|
import {initGltfViewer} from './render/gltf.js';
|
||||||
|
|
||||||
import {initUserAuthOauth2, initUserAuth} from './features/user-auth.js';
|
import {initUserAuthOauth2, initUserAuth} from './features/user-auth.js';
|
||||||
import {
|
import {
|
||||||
|
@ -187,6 +188,7 @@ onDomReady(() => {
|
||||||
initUserAuth();
|
initUserAuth();
|
||||||
initRepoDiffView();
|
initRepoDiffView();
|
||||||
initPdfViewer();
|
initPdfViewer();
|
||||||
|
initGltfViewer();
|
||||||
initScopedAccessTokenCategories();
|
initScopedAccessTokenCategories();
|
||||||
initColorPickers();
|
initColorPickers();
|
||||||
});
|
});
|
||||||
|
|
6
web_src/js/render/gltf.js
Normal file
6
web_src/js/render/gltf.js
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
export async function initGltfViewer() {
|
||||||
|
const els = document.querySelectorAll('model-viewer');
|
||||||
|
if (!els.length) return;
|
||||||
|
|
||||||
|
await import(/* webpackChunkName: "@google/model-viewer" */'@google/model-viewer');
|
||||||
|
}
|
Loading…
Add table
Add a link
Reference in a new issue