mirror of
https://github.com/go-gitea/gitea
synced 2024-12-04 09:57:49 +01:00
Merge branch 'main' into lunny/fix_get_reviewers_bug
This commit is contained in:
commit
e2cd24e08c
69 changed files with 636 additions and 478 deletions
8
.github/workflows/pull-compliance.yml
vendored
8
.github/workflows/pull-compliance.yml
vendored
|
@ -37,7 +37,7 @@ jobs:
|
||||||
python-version: "3.12"
|
python-version: "3.12"
|
||||||
- uses: actions/setup-node@v4
|
- uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
node-version: 20
|
node-version: 22
|
||||||
cache: npm
|
cache: npm
|
||||||
cache-dependency-path: package-lock.json
|
cache-dependency-path: package-lock.json
|
||||||
- run: pip install poetry
|
- run: pip install poetry
|
||||||
|
@ -66,7 +66,7 @@ jobs:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
- uses: actions/setup-node@v4
|
- uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
node-version: 20
|
node-version: 22
|
||||||
cache: npm
|
cache: npm
|
||||||
cache-dependency-path: package-lock.json
|
cache-dependency-path: package-lock.json
|
||||||
- run: make deps-frontend
|
- run: make deps-frontend
|
||||||
|
@ -137,7 +137,7 @@ jobs:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
- uses: actions/setup-node@v4
|
- uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
node-version: 20
|
node-version: 22
|
||||||
cache: npm
|
cache: npm
|
||||||
cache-dependency-path: package-lock.json
|
cache-dependency-path: package-lock.json
|
||||||
- run: make deps-frontend
|
- run: make deps-frontend
|
||||||
|
@ -186,7 +186,7 @@ jobs:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
- uses: actions/setup-node@v4
|
- uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
node-version: 20
|
node-version: 22
|
||||||
cache: npm
|
cache: npm
|
||||||
cache-dependency-path: package-lock.json
|
cache-dependency-path: package-lock.json
|
||||||
- run: make deps-frontend
|
- run: make deps-frontend
|
||||||
|
|
2
.github/workflows/pull-e2e-tests.yml
vendored
2
.github/workflows/pull-e2e-tests.yml
vendored
|
@ -23,7 +23,7 @@ jobs:
|
||||||
check-latest: true
|
check-latest: true
|
||||||
- uses: actions/setup-node@v4
|
- uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
node-version: 20
|
node-version: 22
|
||||||
cache: npm
|
cache: npm
|
||||||
cache-dependency-path: package-lock.json
|
cache-dependency-path: package-lock.json
|
||||||
- run: make deps-frontend frontend deps-backend
|
- run: make deps-frontend frontend deps-backend
|
||||||
|
|
2
.github/workflows/release-nightly.yml
vendored
2
.github/workflows/release-nightly.yml
vendored
|
@ -22,7 +22,7 @@ jobs:
|
||||||
check-latest: true
|
check-latest: true
|
||||||
- uses: actions/setup-node@v4
|
- uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
node-version: 20
|
node-version: 22
|
||||||
cache: npm
|
cache: npm
|
||||||
cache-dependency-path: package-lock.json
|
cache-dependency-path: package-lock.json
|
||||||
- run: make deps-frontend deps-backend
|
- run: make deps-frontend deps-backend
|
||||||
|
|
2
.github/workflows/release-tag-rc.yml
vendored
2
.github/workflows/release-tag-rc.yml
vendored
|
@ -23,7 +23,7 @@ jobs:
|
||||||
check-latest: true
|
check-latest: true
|
||||||
- uses: actions/setup-node@v4
|
- uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
node-version: 20
|
node-version: 22
|
||||||
cache: npm
|
cache: npm
|
||||||
cache-dependency-path: package-lock.json
|
cache-dependency-path: package-lock.json
|
||||||
- run: make deps-frontend deps-backend
|
- run: make deps-frontend deps-backend
|
||||||
|
|
2
.github/workflows/release-tag-version.yml
vendored
2
.github/workflows/release-tag-version.yml
vendored
|
@ -25,7 +25,7 @@ jobs:
|
||||||
check-latest: true
|
check-latest: true
|
||||||
- uses: actions/setup-node@v4
|
- uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
node-version: 20
|
node-version: 22
|
||||||
cache: npm
|
cache: npm
|
||||||
cache-dependency-path: package-lock.json
|
cache-dependency-path: package-lock.json
|
||||||
- run: make deps-frontend deps-backend
|
- run: make deps-frontend deps-backend
|
||||||
|
|
12
flake.lock
12
flake.lock
|
@ -5,11 +5,11 @@
|
||||||
"systems": "systems"
|
"systems": "systems"
|
||||||
},
|
},
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1710146030,
|
"lastModified": 1726560853,
|
||||||
"narHash": "sha256-SZ5L6eA7HJ/nmkzGG7/ISclqe6oZdOZTNoesiInkXPQ=",
|
"narHash": "sha256-X6rJYSESBVr3hBoH0WbKE5KvhPU5bloyZ2L4K60/fPQ=",
|
||||||
"owner": "numtide",
|
"owner": "numtide",
|
||||||
"repo": "flake-utils",
|
"repo": "flake-utils",
|
||||||
"rev": "b1d9ab70662946ef0850d488da1c9019f3a9752a",
|
"rev": "c1dfcf08411b08f6b8615f7d8971a2bfa81d5e8a",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
|
@ -20,11 +20,11 @@
|
||||||
},
|
},
|
||||||
"nixpkgs": {
|
"nixpkgs": {
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1720542800,
|
"lastModified": 1731139594,
|
||||||
"narHash": "sha256-ZgnNHuKV6h2+fQ5LuqnUaqZey1Lqqt5dTUAiAnqH0QQ=",
|
"narHash": "sha256-IigrKK3vYRpUu+HEjPL/phrfh7Ox881er1UEsZvw9Q4=",
|
||||||
"owner": "nixos",
|
"owner": "nixos",
|
||||||
"repo": "nixpkgs",
|
"repo": "nixpkgs",
|
||||||
"rev": "feb2849fdeb70028c70d73b848214b00d324a497",
|
"rev": "76612b17c0ce71689921ca12d9ffdc9c23ce40b2",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
|
|
|
@ -22,7 +22,7 @@
|
||||||
gzip
|
gzip
|
||||||
|
|
||||||
# frontend
|
# frontend
|
||||||
nodejs_20
|
nodejs_22
|
||||||
|
|
||||||
# linting
|
# linting
|
||||||
python312
|
python312
|
||||||
|
|
|
@ -261,6 +261,7 @@ func CancelPreviousJobs(ctx context.Context, repoID int64, ref, workflowID strin
|
||||||
}
|
}
|
||||||
|
|
||||||
// InsertRun inserts a run
|
// InsertRun inserts a run
|
||||||
|
// The title will be cut off at 255 characters if it's longer than 255 characters.
|
||||||
func InsertRun(ctx context.Context, run *ActionRun, jobs []*jobparser.SingleWorkflow) error {
|
func InsertRun(ctx context.Context, run *ActionRun, jobs []*jobparser.SingleWorkflow) error {
|
||||||
ctx, committer, err := db.TxContext(ctx)
|
ctx, committer, err := db.TxContext(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -273,6 +274,7 @@ func InsertRun(ctx context.Context, run *ActionRun, jobs []*jobparser.SingleWork
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
run.Index = index
|
run.Index = index
|
||||||
|
run.Title, _ = util.SplitStringAtByteN(run.Title, 255)
|
||||||
|
|
||||||
if err := db.Insert(ctx, run); err != nil {
|
if err := db.Insert(ctx, run); err != nil {
|
||||||
return err
|
return err
|
||||||
|
@ -399,6 +401,7 @@ func UpdateRun(ctx context.Context, run *ActionRun, cols ...string) error {
|
||||||
if len(cols) > 0 {
|
if len(cols) > 0 {
|
||||||
sess.Cols(cols...)
|
sess.Cols(cols...)
|
||||||
}
|
}
|
||||||
|
run.Title, _ = util.SplitStringAtByteN(run.Title, 255)
|
||||||
affected, err := sess.Update(run)
|
affected, err := sess.Update(run)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
|
|
@ -252,6 +252,7 @@ func GetRunnerByID(ctx context.Context, id int64) (*ActionRunner, error) {
|
||||||
// UpdateRunner updates runner's information.
|
// UpdateRunner updates runner's information.
|
||||||
func UpdateRunner(ctx context.Context, r *ActionRunner, cols ...string) error {
|
func UpdateRunner(ctx context.Context, r *ActionRunner, cols ...string) error {
|
||||||
e := db.GetEngine(ctx)
|
e := db.GetEngine(ctx)
|
||||||
|
r.Name, _ = util.SplitStringAtByteN(r.Name, 255)
|
||||||
var err error
|
var err error
|
||||||
if len(cols) == 0 {
|
if len(cols) == 0 {
|
||||||
_, err = e.ID(r.ID).AllCols().Update(r)
|
_, err = e.ID(r.ID).AllCols().Update(r)
|
||||||
|
@ -278,6 +279,7 @@ func CreateRunner(ctx context.Context, t *ActionRunner) error {
|
||||||
// Remove OwnerID to avoid confusion; it's not worth returning an error here.
|
// Remove OwnerID to avoid confusion; it's not worth returning an error here.
|
||||||
t.OwnerID = 0
|
t.OwnerID = 0
|
||||||
}
|
}
|
||||||
|
t.Name, _ = util.SplitStringAtByteN(t.Name, 255)
|
||||||
return db.Insert(ctx, t)
|
return db.Insert(ctx, t)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -12,6 +12,7 @@ import (
|
||||||
repo_model "code.gitea.io/gitea/models/repo"
|
repo_model "code.gitea.io/gitea/models/repo"
|
||||||
user_model "code.gitea.io/gitea/models/user"
|
user_model "code.gitea.io/gitea/models/user"
|
||||||
"code.gitea.io/gitea/modules/timeutil"
|
"code.gitea.io/gitea/modules/timeutil"
|
||||||
|
"code.gitea.io/gitea/modules/util"
|
||||||
webhook_module "code.gitea.io/gitea/modules/webhook"
|
webhook_module "code.gitea.io/gitea/modules/webhook"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -67,6 +68,7 @@ func CreateScheduleTask(ctx context.Context, rows []*ActionSchedule) error {
|
||||||
|
|
||||||
// Loop through each schedule row
|
// Loop through each schedule row
|
||||||
for _, row := range rows {
|
for _, row := range rows {
|
||||||
|
row.Title, _ = util.SplitStringAtByteN(row.Title, 255)
|
||||||
// Create new schedule row
|
// Create new schedule row
|
||||||
if err = db.Insert(ctx, row); err != nil {
|
if err = db.Insert(ctx, row); err != nil {
|
||||||
return err
|
return err
|
||||||
|
|
|
@ -251,6 +251,9 @@ func (a *Action) GetActDisplayNameTitle(ctx context.Context) string {
|
||||||
// GetRepoUserName returns the name of the action repository owner.
|
// GetRepoUserName returns the name of the action repository owner.
|
||||||
func (a *Action) GetRepoUserName(ctx context.Context) string {
|
func (a *Action) GetRepoUserName(ctx context.Context) string {
|
||||||
a.loadRepo(ctx)
|
a.loadRepo(ctx)
|
||||||
|
if a.Repo == nil {
|
||||||
|
return "(non-existing-repo)"
|
||||||
|
}
|
||||||
return a.Repo.OwnerName
|
return a.Repo.OwnerName
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -263,6 +266,9 @@ func (a *Action) ShortRepoUserName(ctx context.Context) string {
|
||||||
// GetRepoName returns the name of the action repository.
|
// GetRepoName returns the name of the action repository.
|
||||||
func (a *Action) GetRepoName(ctx context.Context) string {
|
func (a *Action) GetRepoName(ctx context.Context) string {
|
||||||
a.loadRepo(ctx)
|
a.loadRepo(ctx)
|
||||||
|
if a.Repo == nil {
|
||||||
|
return "(non-existing-repo)"
|
||||||
|
}
|
||||||
return a.Repo.Name
|
return a.Repo.Name
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -18,6 +18,7 @@ import (
|
||||||
"code.gitea.io/gitea/modules/timeutil"
|
"code.gitea.io/gitea/modules/timeutil"
|
||||||
|
|
||||||
"xorm.io/builder"
|
"xorm.io/builder"
|
||||||
|
"xorm.io/xorm/schemas"
|
||||||
)
|
)
|
||||||
|
|
||||||
type (
|
type (
|
||||||
|
@ -50,25 +51,64 @@ const (
|
||||||
// Notification represents a notification
|
// Notification represents a notification
|
||||||
type Notification struct {
|
type Notification struct {
|
||||||
ID int64 `xorm:"pk autoincr"`
|
ID int64 `xorm:"pk autoincr"`
|
||||||
UserID int64 `xorm:"INDEX NOT NULL"`
|
UserID int64 `xorm:"NOT NULL"`
|
||||||
RepoID int64 `xorm:"INDEX NOT NULL"`
|
RepoID int64 `xorm:"NOT NULL"`
|
||||||
|
|
||||||
Status NotificationStatus `xorm:"SMALLINT INDEX NOT NULL"`
|
Status NotificationStatus `xorm:"SMALLINT NOT NULL"`
|
||||||
Source NotificationSource `xorm:"SMALLINT INDEX NOT NULL"`
|
Source NotificationSource `xorm:"SMALLINT NOT NULL"`
|
||||||
|
|
||||||
IssueID int64 `xorm:"INDEX NOT NULL"`
|
IssueID int64 `xorm:"NOT NULL"`
|
||||||
CommitID string `xorm:"INDEX"`
|
CommitID string
|
||||||
CommentID int64
|
CommentID int64
|
||||||
|
|
||||||
UpdatedBy int64 `xorm:"INDEX NOT NULL"`
|
UpdatedBy int64 `xorm:"NOT NULL"`
|
||||||
|
|
||||||
Issue *issues_model.Issue `xorm:"-"`
|
Issue *issues_model.Issue `xorm:"-"`
|
||||||
Repository *repo_model.Repository `xorm:"-"`
|
Repository *repo_model.Repository `xorm:"-"`
|
||||||
Comment *issues_model.Comment `xorm:"-"`
|
Comment *issues_model.Comment `xorm:"-"`
|
||||||
User *user_model.User `xorm:"-"`
|
User *user_model.User `xorm:"-"`
|
||||||
|
|
||||||
CreatedUnix timeutil.TimeStamp `xorm:"created INDEX NOT NULL"`
|
CreatedUnix timeutil.TimeStamp `xorm:"created NOT NULL"`
|
||||||
UpdatedUnix timeutil.TimeStamp `xorm:"updated INDEX NOT NULL"`
|
UpdatedUnix timeutil.TimeStamp `xorm:"updated NOT NULL"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// TableIndices implements xorm's TableIndices interface
|
||||||
|
func (n *Notification) TableIndices() []*schemas.Index {
|
||||||
|
indices := make([]*schemas.Index, 0, 8)
|
||||||
|
usuuIndex := schemas.NewIndex("u_s_uu", schemas.IndexType)
|
||||||
|
usuuIndex.AddColumn("user_id", "status", "updated_unix")
|
||||||
|
indices = append(indices, usuuIndex)
|
||||||
|
|
||||||
|
// Add the individual indices that were previously defined in struct tags
|
||||||
|
userIDIndex := schemas.NewIndex("idx_notification_user_id", schemas.IndexType)
|
||||||
|
userIDIndex.AddColumn("user_id")
|
||||||
|
indices = append(indices, userIDIndex)
|
||||||
|
|
||||||
|
repoIDIndex := schemas.NewIndex("idx_notification_repo_id", schemas.IndexType)
|
||||||
|
repoIDIndex.AddColumn("repo_id")
|
||||||
|
indices = append(indices, repoIDIndex)
|
||||||
|
|
||||||
|
statusIndex := schemas.NewIndex("idx_notification_status", schemas.IndexType)
|
||||||
|
statusIndex.AddColumn("status")
|
||||||
|
indices = append(indices, statusIndex)
|
||||||
|
|
||||||
|
sourceIndex := schemas.NewIndex("idx_notification_source", schemas.IndexType)
|
||||||
|
sourceIndex.AddColumn("source")
|
||||||
|
indices = append(indices, sourceIndex)
|
||||||
|
|
||||||
|
issueIDIndex := schemas.NewIndex("idx_notification_issue_id", schemas.IndexType)
|
||||||
|
issueIDIndex.AddColumn("issue_id")
|
||||||
|
indices = append(indices, issueIDIndex)
|
||||||
|
|
||||||
|
commitIDIndex := schemas.NewIndex("idx_notification_commit_id", schemas.IndexType)
|
||||||
|
commitIDIndex.AddColumn("commit_id")
|
||||||
|
indices = append(indices, commitIDIndex)
|
||||||
|
|
||||||
|
updatedByIndex := schemas.NewIndex("idx_notification_updated_by", schemas.IndexType)
|
||||||
|
updatedByIndex.AddColumn("updated_by")
|
||||||
|
indices = append(indices, updatedByIndex)
|
||||||
|
|
||||||
|
return indices
|
||||||
}
|
}
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
|
|
|
@ -21,6 +21,7 @@ import (
|
||||||
"code.gitea.io/gitea/modules/references"
|
"code.gitea.io/gitea/modules/references"
|
||||||
api "code.gitea.io/gitea/modules/structs"
|
api "code.gitea.io/gitea/modules/structs"
|
||||||
"code.gitea.io/gitea/modules/timeutil"
|
"code.gitea.io/gitea/modules/timeutil"
|
||||||
|
"code.gitea.io/gitea/modules/util"
|
||||||
|
|
||||||
"xorm.io/builder"
|
"xorm.io/builder"
|
||||||
)
|
)
|
||||||
|
@ -138,6 +139,7 @@ func ChangeIssueTitle(ctx context.Context, issue *Issue, doer *user_model.User,
|
||||||
}
|
}
|
||||||
defer committer.Close()
|
defer committer.Close()
|
||||||
|
|
||||||
|
issue.Title, _ = util.SplitStringAtByteN(issue.Title, 255)
|
||||||
if err = UpdateIssueCols(ctx, issue, "name"); err != nil {
|
if err = UpdateIssueCols(ctx, issue, "name"); err != nil {
|
||||||
return fmt.Errorf("updateIssueCols: %w", err)
|
return fmt.Errorf("updateIssueCols: %w", err)
|
||||||
}
|
}
|
||||||
|
@ -386,6 +388,7 @@ func NewIssueWithIndex(ctx context.Context, doer *user_model.User, opts NewIssue
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewIssue creates new issue with labels for repository.
|
// NewIssue creates new issue with labels for repository.
|
||||||
|
// The title will be cut off at 255 characters if it's longer than 255 characters.
|
||||||
func NewIssue(ctx context.Context, repo *repo_model.Repository, issue *Issue, labelIDs []int64, uuids []string) (err error) {
|
func NewIssue(ctx context.Context, repo *repo_model.Repository, issue *Issue, labelIDs []int64, uuids []string) (err error) {
|
||||||
ctx, committer, err := db.TxContext(ctx)
|
ctx, committer, err := db.TxContext(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -399,6 +402,7 @@ func NewIssue(ctx context.Context, repo *repo_model.Repository, issue *Issue, la
|
||||||
}
|
}
|
||||||
|
|
||||||
issue.Index = idx
|
issue.Index = idx
|
||||||
|
issue.Title, _ = util.SplitStringAtByteN(issue.Title, 255)
|
||||||
|
|
||||||
if err = NewIssueWithIndex(ctx, issue.Poster, NewIssueOptions{
|
if err = NewIssueWithIndex(ctx, issue.Poster, NewIssueOptions{
|
||||||
Repo: repo,
|
Repo: repo,
|
||||||
|
|
|
@ -572,6 +572,7 @@ func NewPullRequest(ctx context.Context, repo *repo_model.Repository, issue *Iss
|
||||||
}
|
}
|
||||||
|
|
||||||
issue.Index = idx
|
issue.Index = idx
|
||||||
|
issue.Title, _ = util.SplitStringAtByteN(issue.Title, 255)
|
||||||
|
|
||||||
if err = NewIssueWithIndex(ctx, issue.Poster, NewIssueOptions{
|
if err = NewIssueWithIndex(ctx, issue.Poster, NewIssueOptions{
|
||||||
Repo: repo,
|
Repo: repo,
|
||||||
|
|
|
@ -366,6 +366,7 @@ func prepareMigrationTasks() []*migration {
|
||||||
newMigration(306, "Add BlockAdminMergeOverride to ProtectedBranch", v1_23.AddBlockAdminMergeOverrideBranchProtection),
|
newMigration(306, "Add BlockAdminMergeOverride to ProtectedBranch", v1_23.AddBlockAdminMergeOverrideBranchProtection),
|
||||||
newMigration(307, "Fix milestone deadline_unix when there is no due date", v1_23.FixMilestoneNoDueDate),
|
newMigration(307, "Fix milestone deadline_unix when there is no due date", v1_23.FixMilestoneNoDueDate),
|
||||||
newMigration(308, "Add index(user_id, is_deleted) for action table", v1_23.AddNewIndexForUserDashboard),
|
newMigration(308, "Add index(user_id, is_deleted) for action table", v1_23.AddNewIndexForUserDashboard),
|
||||||
|
newMigration(309, "Improve Notification table indices", v1_23.ImproveNotificationTableIndices),
|
||||||
}
|
}
|
||||||
return preparedMigrations
|
return preparedMigrations
|
||||||
}
|
}
|
||||||
|
|
77
models/migrations/v1_23/v309.go
Normal file
77
models/migrations/v1_23/v309.go
Normal file
|
@ -0,0 +1,77 @@
|
||||||
|
// Copyright 2024 The Gitea Authors. All rights reserved.
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
package v1_23 //nolint
|
||||||
|
|
||||||
|
import (
|
||||||
|
"code.gitea.io/gitea/modules/timeutil"
|
||||||
|
|
||||||
|
"xorm.io/xorm"
|
||||||
|
"xorm.io/xorm/schemas"
|
||||||
|
)
|
||||||
|
|
||||||
|
type improveNotificationTableIndicesAction struct {
|
||||||
|
ID int64 `xorm:"pk autoincr"`
|
||||||
|
UserID int64 `xorm:"NOT NULL"`
|
||||||
|
RepoID int64 `xorm:"NOT NULL"`
|
||||||
|
|
||||||
|
Status uint8 `xorm:"SMALLINT NOT NULL"`
|
||||||
|
Source uint8 `xorm:"SMALLINT NOT NULL"`
|
||||||
|
|
||||||
|
IssueID int64 `xorm:"NOT NULL"`
|
||||||
|
CommitID string
|
||||||
|
CommentID int64
|
||||||
|
|
||||||
|
UpdatedBy int64 `xorm:"NOT NULL"`
|
||||||
|
|
||||||
|
CreatedUnix timeutil.TimeStamp `xorm:"created NOT NULL"`
|
||||||
|
UpdatedUnix timeutil.TimeStamp `xorm:"updated NOT NULL"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// TableName sets the name of this table
|
||||||
|
func (*improveNotificationTableIndicesAction) TableName() string {
|
||||||
|
return "notification"
|
||||||
|
}
|
||||||
|
|
||||||
|
// TableIndices implements xorm's TableIndices interface
|
||||||
|
func (*improveNotificationTableIndicesAction) TableIndices() []*schemas.Index {
|
||||||
|
indices := make([]*schemas.Index, 0, 8)
|
||||||
|
usuuIndex := schemas.NewIndex("u_s_uu", schemas.IndexType)
|
||||||
|
usuuIndex.AddColumn("user_id", "status", "updated_unix")
|
||||||
|
indices = append(indices, usuuIndex)
|
||||||
|
|
||||||
|
// Add the individual indices that were previously defined in struct tags
|
||||||
|
userIDIndex := schemas.NewIndex("idx_notification_user_id", schemas.IndexType)
|
||||||
|
userIDIndex.AddColumn("user_id")
|
||||||
|
indices = append(indices, userIDIndex)
|
||||||
|
|
||||||
|
repoIDIndex := schemas.NewIndex("idx_notification_repo_id", schemas.IndexType)
|
||||||
|
repoIDIndex.AddColumn("repo_id")
|
||||||
|
indices = append(indices, repoIDIndex)
|
||||||
|
|
||||||
|
statusIndex := schemas.NewIndex("idx_notification_status", schemas.IndexType)
|
||||||
|
statusIndex.AddColumn("status")
|
||||||
|
indices = append(indices, statusIndex)
|
||||||
|
|
||||||
|
sourceIndex := schemas.NewIndex("idx_notification_source", schemas.IndexType)
|
||||||
|
sourceIndex.AddColumn("source")
|
||||||
|
indices = append(indices, sourceIndex)
|
||||||
|
|
||||||
|
issueIDIndex := schemas.NewIndex("idx_notification_issue_id", schemas.IndexType)
|
||||||
|
issueIDIndex.AddColumn("issue_id")
|
||||||
|
indices = append(indices, issueIDIndex)
|
||||||
|
|
||||||
|
commitIDIndex := schemas.NewIndex("idx_notification_commit_id", schemas.IndexType)
|
||||||
|
commitIDIndex.AddColumn("commit_id")
|
||||||
|
indices = append(indices, commitIDIndex)
|
||||||
|
|
||||||
|
updatedByIndex := schemas.NewIndex("idx_notification_updated_by", schemas.IndexType)
|
||||||
|
updatedByIndex.AddColumn("updated_by")
|
||||||
|
indices = append(indices, updatedByIndex)
|
||||||
|
|
||||||
|
return indices
|
||||||
|
}
|
||||||
|
|
||||||
|
func ImproveNotificationTableIndices(x *xorm.Engine) error {
|
||||||
|
return x.Sync(&improveNotificationTableIndicesAction{})
|
||||||
|
}
|
|
@ -1,78 +0,0 @@
|
||||||
// Copyright 2022 The Gitea Authors. All rights reserved.
|
|
||||||
// SPDX-License-Identifier: MIT
|
|
||||||
|
|
||||||
package organization
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"fmt"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"code.gitea.io/gitea/models/db"
|
|
||||||
repo_model "code.gitea.io/gitea/models/repo"
|
|
||||||
"code.gitea.io/gitea/models/unit"
|
|
||||||
user_model "code.gitea.io/gitea/models/user"
|
|
||||||
|
|
||||||
"xorm.io/builder"
|
|
||||||
)
|
|
||||||
|
|
||||||
// MinimalOrg represents a simple organization with only the needed columns
|
|
||||||
type MinimalOrg = Organization
|
|
||||||
|
|
||||||
// GetUserOrgsList returns all organizations the given user has access to
|
|
||||||
func GetUserOrgsList(ctx context.Context, user *user_model.User) ([]*MinimalOrg, error) {
|
|
||||||
schema, err := db.TableInfo(new(user_model.User))
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
outputCols := []string{
|
|
||||||
"id",
|
|
||||||
"name",
|
|
||||||
"full_name",
|
|
||||||
"visibility",
|
|
||||||
"avatar",
|
|
||||||
"avatar_email",
|
|
||||||
"use_custom_avatar",
|
|
||||||
}
|
|
||||||
|
|
||||||
groupByCols := &strings.Builder{}
|
|
||||||
for _, col := range outputCols {
|
|
||||||
fmt.Fprintf(groupByCols, "`%s`.%s,", schema.Name, col)
|
|
||||||
}
|
|
||||||
groupByStr := groupByCols.String()
|
|
||||||
groupByStr = groupByStr[0 : len(groupByStr)-1]
|
|
||||||
|
|
||||||
sess := db.GetEngine(ctx)
|
|
||||||
sess = sess.Select(groupByStr+", count(distinct repo_id) as org_count").
|
|
||||||
Table("user").
|
|
||||||
Join("INNER", "team", "`team`.org_id = `user`.id").
|
|
||||||
Join("INNER", "team_user", "`team`.id = `team_user`.team_id").
|
|
||||||
Join("LEFT", builder.
|
|
||||||
Select("id as repo_id, owner_id as repo_owner_id").
|
|
||||||
From("repository").
|
|
||||||
Where(repo_model.AccessibleRepositoryCondition(user, unit.TypeInvalid)), "`repository`.repo_owner_id = `team`.org_id").
|
|
||||||
Where("`team_user`.uid = ?", user.ID).
|
|
||||||
GroupBy(groupByStr)
|
|
||||||
|
|
||||||
type OrgCount struct {
|
|
||||||
Organization `xorm:"extends"`
|
|
||||||
OrgCount int
|
|
||||||
}
|
|
||||||
|
|
||||||
orgCounts := make([]*OrgCount, 0, 10)
|
|
||||||
|
|
||||||
if err := sess.
|
|
||||||
Asc("`user`.name").
|
|
||||||
Find(&orgCounts); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
orgs := make([]*MinimalOrg, len(orgCounts))
|
|
||||||
for i, orgCount := range orgCounts {
|
|
||||||
orgCount.Organization.NumRepos = orgCount.OrgCount
|
|
||||||
orgs[i] = &orgCount.Organization
|
|
||||||
}
|
|
||||||
|
|
||||||
return orgs, nil
|
|
||||||
}
|
|
|
@ -25,13 +25,6 @@ import (
|
||||||
"xorm.io/xorm"
|
"xorm.io/xorm"
|
||||||
)
|
)
|
||||||
|
|
||||||
// ________ .__ __ .__
|
|
||||||
// \_____ \_______ _________ ____ |__|____________ _/ |_|__| ____ ____
|
|
||||||
// / | \_ __ \/ ___\__ \ / \| \___ /\__ \\ __\ |/ _ \ / \
|
|
||||||
// / | \ | \/ /_/ > __ \| | \ |/ / / __ \| | | ( <_> ) | \
|
|
||||||
// \_______ /__| \___ (____ /___| /__/_____ \(____ /__| |__|\____/|___| /
|
|
||||||
// \/ /_____/ \/ \/ \/ \/ \/
|
|
||||||
|
|
||||||
// ErrOrgNotExist represents a "OrgNotExist" kind of error.
|
// ErrOrgNotExist represents a "OrgNotExist" kind of error.
|
||||||
type ErrOrgNotExist struct {
|
type ErrOrgNotExist struct {
|
||||||
ID int64
|
ID int64
|
||||||
|
@ -465,42 +458,6 @@ func GetUsersWhoCanCreateOrgRepo(ctx context.Context, orgID int64) (map[int64]*u
|
||||||
And("team_user.org_id = ?", orgID).Find(&users)
|
And("team_user.org_id = ?", orgID).Find(&users)
|
||||||
}
|
}
|
||||||
|
|
||||||
// SearchOrganizationsOptions options to filter organizations
|
|
||||||
type SearchOrganizationsOptions struct {
|
|
||||||
db.ListOptions
|
|
||||||
All bool
|
|
||||||
}
|
|
||||||
|
|
||||||
// FindOrgOptions finds orgs options
|
|
||||||
type FindOrgOptions struct {
|
|
||||||
db.ListOptions
|
|
||||||
UserID int64
|
|
||||||
IncludePrivate bool
|
|
||||||
}
|
|
||||||
|
|
||||||
func queryUserOrgIDs(userID int64, includePrivate bool) *builder.Builder {
|
|
||||||
cond := builder.Eq{"uid": userID}
|
|
||||||
if !includePrivate {
|
|
||||||
cond["is_public"] = true
|
|
||||||
}
|
|
||||||
return builder.Select("org_id").From("org_user").Where(cond)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (opts FindOrgOptions) ToConds() builder.Cond {
|
|
||||||
var cond builder.Cond = builder.Eq{"`user`.`type`": user_model.UserTypeOrganization}
|
|
||||||
if opts.UserID > 0 {
|
|
||||||
cond = cond.And(builder.In("`user`.`id`", queryUserOrgIDs(opts.UserID, opts.IncludePrivate)))
|
|
||||||
}
|
|
||||||
if !opts.IncludePrivate {
|
|
||||||
cond = cond.And(builder.Eq{"`user`.visibility": structs.VisibleTypePublic})
|
|
||||||
}
|
|
||||||
return cond
|
|
||||||
}
|
|
||||||
|
|
||||||
func (opts FindOrgOptions) ToOrders() string {
|
|
||||||
return "`user`.name ASC"
|
|
||||||
}
|
|
||||||
|
|
||||||
// HasOrgOrUserVisible tells if the given user can see the given org or user
|
// HasOrgOrUserVisible tells if the given user can see the given org or user
|
||||||
func HasOrgOrUserVisible(ctx context.Context, orgOrUser, user *user_model.User) bool {
|
func HasOrgOrUserVisible(ctx context.Context, orgOrUser, user *user_model.User) bool {
|
||||||
// If user is nil, it's an anonymous user/request.
|
// If user is nil, it's an anonymous user/request.
|
||||||
|
@ -533,20 +490,6 @@ func HasOrgsVisible(ctx context.Context, orgs []*Organization, user *user_model.
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetOrgsCanCreateRepoByUserID returns a list of organizations where given user ID
|
|
||||||
// are allowed to create repos.
|
|
||||||
func GetOrgsCanCreateRepoByUserID(ctx context.Context, userID int64) ([]*Organization, error) {
|
|
||||||
orgs := make([]*Organization, 0, 10)
|
|
||||||
|
|
||||||
return orgs, db.GetEngine(ctx).Where(builder.In("id", builder.Select("`user`.id").From("`user`").
|
|
||||||
Join("INNER", "`team_user`", "`team_user`.org_id = `user`.id").
|
|
||||||
Join("INNER", "`team`", "`team`.id = `team_user`.team_id").
|
|
||||||
Where(builder.Eq{"`team_user`.uid": userID}).
|
|
||||||
And(builder.Eq{"`team`.authorize": perm.AccessModeOwner}.Or(builder.Eq{"`team`.can_create_org_repo": true})))).
|
|
||||||
Asc("`user`.name").
|
|
||||||
Find(&orgs)
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetOrgUsersByOrgID returns all organization-user relations by organization ID.
|
// GetOrgUsersByOrgID returns all organization-user relations by organization ID.
|
||||||
func GetOrgUsersByOrgID(ctx context.Context, opts *FindOrgMembersOpts) ([]*OrgUser, error) {
|
func GetOrgUsersByOrgID(ctx context.Context, opts *FindOrgMembersOpts) ([]*OrgUser, error) {
|
||||||
sess := db.GetEngine(ctx).Where("org_id=?", opts.OrgID)
|
sess := db.GetEngine(ctx).Where("org_id=?", opts.OrgID)
|
||||||
|
|
138
models/organization/org_list.go
Normal file
138
models/organization/org_list.go
Normal file
|
@ -0,0 +1,138 @@
|
||||||
|
// Copyright 2024 The Gitea Authors. All rights reserved.
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
package organization
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"code.gitea.io/gitea/models/db"
|
||||||
|
"code.gitea.io/gitea/models/perm"
|
||||||
|
user_model "code.gitea.io/gitea/models/user"
|
||||||
|
"code.gitea.io/gitea/modules/structs"
|
||||||
|
|
||||||
|
"xorm.io/builder"
|
||||||
|
)
|
||||||
|
|
||||||
|
// SearchOrganizationsOptions options to filter organizations
|
||||||
|
type SearchOrganizationsOptions struct {
|
||||||
|
db.ListOptions
|
||||||
|
All bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// FindOrgOptions finds orgs options
|
||||||
|
type FindOrgOptions struct {
|
||||||
|
db.ListOptions
|
||||||
|
UserID int64
|
||||||
|
IncludePrivate bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func queryUserOrgIDs(userID int64, includePrivate bool) *builder.Builder {
|
||||||
|
cond := builder.Eq{"uid": userID}
|
||||||
|
if !includePrivate {
|
||||||
|
cond["is_public"] = true
|
||||||
|
}
|
||||||
|
return builder.Select("org_id").From("org_user").Where(cond)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (opts FindOrgOptions) ToConds() builder.Cond {
|
||||||
|
var cond builder.Cond = builder.Eq{"`user`.`type`": user_model.UserTypeOrganization}
|
||||||
|
if opts.UserID > 0 {
|
||||||
|
cond = cond.And(builder.In("`user`.`id`", queryUserOrgIDs(opts.UserID, opts.IncludePrivate)))
|
||||||
|
}
|
||||||
|
if !opts.IncludePrivate {
|
||||||
|
cond = cond.And(builder.Eq{"`user`.visibility": structs.VisibleTypePublic})
|
||||||
|
}
|
||||||
|
return cond
|
||||||
|
}
|
||||||
|
|
||||||
|
func (opts FindOrgOptions) ToOrders() string {
|
||||||
|
return "`user`.lower_name ASC"
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetOrgsCanCreateRepoByUserID returns a list of organizations where given user ID
|
||||||
|
// are allowed to create repos.
|
||||||
|
func GetOrgsCanCreateRepoByUserID(ctx context.Context, userID int64) ([]*Organization, error) {
|
||||||
|
orgs := make([]*Organization, 0, 10)
|
||||||
|
|
||||||
|
return orgs, db.GetEngine(ctx).Where(builder.In("id", builder.Select("`user`.id").From("`user`").
|
||||||
|
Join("INNER", "`team_user`", "`team_user`.org_id = `user`.id").
|
||||||
|
Join("INNER", "`team`", "`team`.id = `team_user`.team_id").
|
||||||
|
Where(builder.Eq{"`team_user`.uid": userID}).
|
||||||
|
And(builder.Eq{"`team`.authorize": perm.AccessModeOwner}.Or(builder.Eq{"`team`.can_create_org_repo": true})))).
|
||||||
|
Asc("`user`.name").
|
||||||
|
Find(&orgs)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MinimalOrg represents a simple organization with only the needed columns
|
||||||
|
type MinimalOrg = Organization
|
||||||
|
|
||||||
|
// GetUserOrgsList returns all organizations the given user has access to
|
||||||
|
func GetUserOrgsList(ctx context.Context, user *user_model.User) ([]*MinimalOrg, error) {
|
||||||
|
schema, err := db.TableInfo(new(user_model.User))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
outputCols := []string{
|
||||||
|
"id",
|
||||||
|
"name",
|
||||||
|
"full_name",
|
||||||
|
"visibility",
|
||||||
|
"avatar",
|
||||||
|
"avatar_email",
|
||||||
|
"use_custom_avatar",
|
||||||
|
}
|
||||||
|
|
||||||
|
selectColumns := &strings.Builder{}
|
||||||
|
for i, col := range outputCols {
|
||||||
|
fmt.Fprintf(selectColumns, "`%s`.%s", schema.Name, col)
|
||||||
|
if i < len(outputCols)-1 {
|
||||||
|
selectColumns.WriteString(", ")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
columnsStr := selectColumns.String()
|
||||||
|
|
||||||
|
var orgs []*MinimalOrg
|
||||||
|
if err := db.GetEngine(ctx).Select(columnsStr).
|
||||||
|
Table("user").
|
||||||
|
Where(builder.In("`user`.`id`", queryUserOrgIDs(user.ID, true))).
|
||||||
|
Find(&orgs); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
type orgCount struct {
|
||||||
|
OrgID int64
|
||||||
|
RepoCount int
|
||||||
|
}
|
||||||
|
var orgCounts []orgCount
|
||||||
|
if err := db.GetEngine(ctx).
|
||||||
|
Select("owner_id AS org_id, COUNT(DISTINCT(repository.id)) as repo_count").
|
||||||
|
Table("repository").
|
||||||
|
Join("INNER", "org_user", "owner_id = org_user.org_id").
|
||||||
|
Where("org_user.uid = ?", user.ID).
|
||||||
|
And(builder.Or(
|
||||||
|
builder.Eq{"repository.is_private": false},
|
||||||
|
builder.In("repository.id", builder.Select("repo_id").From("team_repo").
|
||||||
|
InnerJoin("team_user", "team_user.team_id = team_repo.team_id").
|
||||||
|
Where(builder.Eq{"team_user.uid": user.ID})),
|
||||||
|
builder.In("repository.id", builder.Select("repo_id").From("collaboration").
|
||||||
|
Where(builder.Eq{"user_id": user.ID})),
|
||||||
|
)).
|
||||||
|
GroupBy("owner_id").Find(&orgCounts); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
orgCountMap := make(map[int64]int, len(orgCounts))
|
||||||
|
for _, orgCount := range orgCounts {
|
||||||
|
orgCountMap[orgCount.OrgID] = orgCount.RepoCount
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, org := range orgs {
|
||||||
|
org.NumRepos = orgCountMap[org.ID]
|
||||||
|
}
|
||||||
|
|
||||||
|
return orgs, nil
|
||||||
|
}
|
62
models/organization/org_list_test.go
Normal file
62
models/organization/org_list_test.go
Normal file
|
@ -0,0 +1,62 @@
|
||||||
|
// Copyright 2024 The Gitea Authors. All rights reserved.
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
package organization_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"code.gitea.io/gitea/models/db"
|
||||||
|
"code.gitea.io/gitea/models/organization"
|
||||||
|
"code.gitea.io/gitea/models/unittest"
|
||||||
|
user_model "code.gitea.io/gitea/models/user"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestCountOrganizations(t *testing.T) {
|
||||||
|
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||||
|
expected, err := db.GetEngine(db.DefaultContext).Where("type=?", user_model.UserTypeOrganization).Count(&organization.Organization{})
|
||||||
|
assert.NoError(t, err)
|
||||||
|
cnt, err := db.Count[organization.Organization](db.DefaultContext, organization.FindOrgOptions{IncludePrivate: true})
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, expected, cnt)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFindOrgs(t *testing.T) {
|
||||||
|
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||||
|
|
||||||
|
orgs, err := db.Find[organization.Organization](db.DefaultContext, organization.FindOrgOptions{
|
||||||
|
UserID: 4,
|
||||||
|
IncludePrivate: true,
|
||||||
|
})
|
||||||
|
assert.NoError(t, err)
|
||||||
|
if assert.Len(t, orgs, 1) {
|
||||||
|
assert.EqualValues(t, 3, orgs[0].ID)
|
||||||
|
}
|
||||||
|
|
||||||
|
orgs, err = db.Find[organization.Organization](db.DefaultContext, organization.FindOrgOptions{
|
||||||
|
UserID: 4,
|
||||||
|
IncludePrivate: false,
|
||||||
|
})
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Len(t, orgs, 0)
|
||||||
|
|
||||||
|
total, err := db.Count[organization.Organization](db.DefaultContext, organization.FindOrgOptions{
|
||||||
|
UserID: 4,
|
||||||
|
IncludePrivate: true,
|
||||||
|
})
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.EqualValues(t, 1, total)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetUserOrgsList(t *testing.T) {
|
||||||
|
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||||
|
orgs, err := organization.GetUserOrgsList(db.DefaultContext, &user_model.User{ID: 4})
|
||||||
|
assert.NoError(t, err)
|
||||||
|
if assert.Len(t, orgs, 1) {
|
||||||
|
assert.EqualValues(t, 3, orgs[0].ID)
|
||||||
|
// repo_id: 3 is in the team, 32 is public, 5 is private with no team
|
||||||
|
assert.EqualValues(t, 2, orgs[0].NumRepos)
|
||||||
|
}
|
||||||
|
}
|
|
@ -129,15 +129,6 @@ func TestGetOrgByName(t *testing.T) {
|
||||||
assert.True(t, organization.IsErrOrgNotExist(err))
|
assert.True(t, organization.IsErrOrgNotExist(err))
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestCountOrganizations(t *testing.T) {
|
|
||||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
|
||||||
expected, err := db.GetEngine(db.DefaultContext).Where("type=?", user_model.UserTypeOrganization).Count(&organization.Organization{})
|
|
||||||
assert.NoError(t, err)
|
|
||||||
cnt, err := db.Count[organization.Organization](db.DefaultContext, organization.FindOrgOptions{IncludePrivate: true})
|
|
||||||
assert.NoError(t, err)
|
|
||||||
assert.Equal(t, expected, cnt)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestIsOrganizationOwner(t *testing.T) {
|
func TestIsOrganizationOwner(t *testing.T) {
|
||||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||||
test := func(orgID, userID int64, expected bool) {
|
test := func(orgID, userID int64, expected bool) {
|
||||||
|
@ -251,33 +242,6 @@ func TestRestrictedUserOrgMembers(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestFindOrgs(t *testing.T) {
|
|
||||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
|
||||||
|
|
||||||
orgs, err := db.Find[organization.Organization](db.DefaultContext, organization.FindOrgOptions{
|
|
||||||
UserID: 4,
|
|
||||||
IncludePrivate: true,
|
|
||||||
})
|
|
||||||
assert.NoError(t, err)
|
|
||||||
if assert.Len(t, orgs, 1) {
|
|
||||||
assert.EqualValues(t, 3, orgs[0].ID)
|
|
||||||
}
|
|
||||||
|
|
||||||
orgs, err = db.Find[organization.Organization](db.DefaultContext, organization.FindOrgOptions{
|
|
||||||
UserID: 4,
|
|
||||||
IncludePrivate: false,
|
|
||||||
})
|
|
||||||
assert.NoError(t, err)
|
|
||||||
assert.Len(t, orgs, 0)
|
|
||||||
|
|
||||||
total, err := db.Count[organization.Organization](db.DefaultContext, organization.FindOrgOptions{
|
|
||||||
UserID: 4,
|
|
||||||
IncludePrivate: true,
|
|
||||||
})
|
|
||||||
assert.NoError(t, err)
|
|
||||||
assert.EqualValues(t, 1, total)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestGetOrgUsersByOrgID(t *testing.T) {
|
func TestGetOrgUsersByOrgID(t *testing.T) {
|
||||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||||
|
|
||||||
|
|
|
@ -242,6 +242,7 @@ func GetSearchOrderByBySortType(sortType string) db.SearchOrderBy {
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewProject creates a new Project
|
// NewProject creates a new Project
|
||||||
|
// The title will be cut off at 255 characters if it's longer than 255 characters.
|
||||||
func NewProject(ctx context.Context, p *Project) error {
|
func NewProject(ctx context.Context, p *Project) error {
|
||||||
if !IsTemplateTypeValid(p.TemplateType) {
|
if !IsTemplateTypeValid(p.TemplateType) {
|
||||||
p.TemplateType = TemplateTypeNone
|
p.TemplateType = TemplateTypeNone
|
||||||
|
@ -255,6 +256,8 @@ func NewProject(ctx context.Context, p *Project) error {
|
||||||
return util.NewInvalidArgumentErrorf("project type is not valid")
|
return util.NewInvalidArgumentErrorf("project type is not valid")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
p.Title, _ = util.SplitStringAtByteN(p.Title, 255)
|
||||||
|
|
||||||
return db.WithTx(ctx, func(ctx context.Context) error {
|
return db.WithTx(ctx, func(ctx context.Context) error {
|
||||||
if err := db.Insert(ctx, p); err != nil {
|
if err := db.Insert(ctx, p); err != nil {
|
||||||
return err
|
return err
|
||||||
|
@ -308,6 +311,7 @@ func UpdateProject(ctx context.Context, p *Project) error {
|
||||||
p.CardType = CardTypeTextOnly
|
p.CardType = CardTypeTextOnly
|
||||||
}
|
}
|
||||||
|
|
||||||
|
p.Title, _ = util.SplitStringAtByteN(p.Title, 255)
|
||||||
_, err := db.GetEngine(ctx).ID(p.ID).Cols(
|
_, err := db.GetEngine(ctx).ID(p.ID).Cols(
|
||||||
"title",
|
"title",
|
||||||
"description",
|
"description",
|
||||||
|
|
|
@ -156,6 +156,7 @@ func IsReleaseExist(ctx context.Context, repoID int64, tagName string) (bool, er
|
||||||
|
|
||||||
// UpdateRelease updates all columns of a release
|
// UpdateRelease updates all columns of a release
|
||||||
func UpdateRelease(ctx context.Context, rel *Release) error {
|
func UpdateRelease(ctx context.Context, rel *Release) error {
|
||||||
|
rel.Title, _ = util.SplitStringAtByteN(rel.Title, 255)
|
||||||
_, err := db.GetEngine(ctx).ID(rel.ID).AllCols().Update(rel)
|
_, err := db.GetEngine(ctx).ID(rel.ID).AllCols().Update(rel)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
|
@ -479,7 +479,6 @@ func (repo *Repository) ComposeMetas(ctx context.Context) map[string]string {
|
||||||
metas := map[string]string{
|
metas := map[string]string{
|
||||||
"user": repo.OwnerName,
|
"user": repo.OwnerName,
|
||||||
"repo": repo.Name,
|
"repo": repo.Name,
|
||||||
"mode": "comment",
|
|
||||||
}
|
}
|
||||||
|
|
||||||
unit, err := repo.GetUnit(ctx, unit.TypeExternalTracker)
|
unit, err := repo.GetUnit(ctx, unit.TypeExternalTracker)
|
||||||
|
@ -521,7 +520,6 @@ func (repo *Repository) ComposeDocumentMetas(ctx context.Context) map[string]str
|
||||||
for k, v := range repo.ComposeMetas(ctx) {
|
for k, v := range repo.ComposeMetas(ctx) {
|
||||||
metas[k] = v
|
metas[k] = v
|
||||||
}
|
}
|
||||||
metas["mode"] = "document"
|
|
||||||
repo.DocumentRenderingMetas = metas
|
repo.DocumentRenderingMetas = metas
|
||||||
}
|
}
|
||||||
return repo.DocumentRenderingMetas
|
return repo.DocumentRenderingMetas
|
||||||
|
|
|
@ -8,7 +8,6 @@ import (
|
||||||
"io"
|
"io"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"regexp"
|
"regexp"
|
||||||
"strings"
|
|
||||||
|
|
||||||
"code.gitea.io/gitea/modules/markup"
|
"code.gitea.io/gitea/modules/markup"
|
||||||
"code.gitea.io/gitea/modules/setting"
|
"code.gitea.io/gitea/modules/setting"
|
||||||
|
@ -17,9 +16,6 @@ import (
|
||||||
"github.com/go-enry/go-enry/v2"
|
"github.com/go-enry/go-enry/v2"
|
||||||
)
|
)
|
||||||
|
|
||||||
// MarkupName describes markup's name
|
|
||||||
var MarkupName = "console"
|
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
markup.RegisterRenderer(Renderer{})
|
markup.RegisterRenderer(Renderer{})
|
||||||
}
|
}
|
||||||
|
@ -29,7 +25,7 @@ type Renderer struct{}
|
||||||
|
|
||||||
// Name implements markup.Renderer
|
// Name implements markup.Renderer
|
||||||
func (Renderer) Name() string {
|
func (Renderer) Name() string {
|
||||||
return MarkupName
|
return "console"
|
||||||
}
|
}
|
||||||
|
|
||||||
// Extensions implements markup.Renderer
|
// Extensions implements markup.Renderer
|
||||||
|
@ -67,20 +63,3 @@ func (Renderer) Render(ctx *markup.RenderContext, input io.Reader, output io.Wri
|
||||||
_, err = output.Write(buf)
|
_, err = output.Write(buf)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Render renders terminal colors to HTML with all specific handling stuff.
|
|
||||||
func Render(ctx *markup.RenderContext, input io.Reader, output io.Writer) error {
|
|
||||||
if ctx.Type == "" {
|
|
||||||
ctx.Type = MarkupName
|
|
||||||
}
|
|
||||||
return markup.Render(ctx, input, output)
|
|
||||||
}
|
|
||||||
|
|
||||||
// RenderString renders terminal colors in string to HTML with all specific handling stuff and return string
|
|
||||||
func RenderString(ctx *markup.RenderContext, content string) (string, error) {
|
|
||||||
var buf strings.Builder
|
|
||||||
if err := Render(ctx, strings.NewReader(content), &buf); err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
return buf.String(), nil
|
|
||||||
}
|
|
||||||
|
|
|
@ -442,12 +442,11 @@ func createLink(href, content, class string) *html.Node {
|
||||||
a := &html.Node{
|
a := &html.Node{
|
||||||
Type: html.ElementNode,
|
Type: html.ElementNode,
|
||||||
Data: atom.A.String(),
|
Data: atom.A.String(),
|
||||||
Attr: []html.Attribute{
|
Attr: []html.Attribute{{Key: "href", Val: href}},
|
||||||
{Key: "href", Val: href},
|
}
|
||||||
{Key: "data-markdown-generated-content"},
|
if !RenderBehaviorForTesting.DisableInternalAttributes {
|
||||||
},
|
a.Attr = append(a.Attr, html.Attribute{Key: "data-markdown-generated-content"})
|
||||||
}
|
}
|
||||||
|
|
||||||
if class != "" {
|
if class != "" {
|
||||||
a.Attr = append(a.Attr, html.Attribute{Key: "class", Val: class})
|
a.Attr = append(a.Attr, html.Attribute{Key: "class", Val: class})
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,6 +11,7 @@ import (
|
||||||
|
|
||||||
"code.gitea.io/gitea/modules/git"
|
"code.gitea.io/gitea/modules/git"
|
||||||
"code.gitea.io/gitea/modules/markup"
|
"code.gitea.io/gitea/modules/markup"
|
||||||
|
"code.gitea.io/gitea/modules/markup/markdown"
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
)
|
)
|
||||||
|
@ -23,8 +24,8 @@ func TestRenderCodePreview(t *testing.T) {
|
||||||
})
|
})
|
||||||
test := func(input, expected string) {
|
test := func(input, expected string) {
|
||||||
buffer, err := markup.RenderString(&markup.RenderContext{
|
buffer, err := markup.RenderString(&markup.RenderContext{
|
||||||
Ctx: git.DefaultContext,
|
Ctx: git.DefaultContext,
|
||||||
Type: "markdown",
|
MarkupType: markdown.MarkupName,
|
||||||
}, input)
|
}, input)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(buffer))
|
assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(buffer))
|
||||||
|
|
|
@ -11,6 +11,7 @@ import (
|
||||||
|
|
||||||
"code.gitea.io/gitea/modules/git"
|
"code.gitea.io/gitea/modules/git"
|
||||||
"code.gitea.io/gitea/modules/setting"
|
"code.gitea.io/gitea/modules/setting"
|
||||||
|
testModule "code.gitea.io/gitea/modules/test"
|
||||||
"code.gitea.io/gitea/modules/util"
|
"code.gitea.io/gitea/modules/util"
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
|
@ -123,8 +124,9 @@ func TestRender_IssueIndexPattern2(t *testing.T) {
|
||||||
}
|
}
|
||||||
expectedNil := fmt.Sprintf(expectedFmt, links...)
|
expectedNil := fmt.Sprintf(expectedFmt, links...)
|
||||||
testRenderIssueIndexPattern(t, s, expectedNil, &RenderContext{
|
testRenderIssueIndexPattern(t, s, expectedNil, &RenderContext{
|
||||||
Ctx: git.DefaultContext,
|
Ctx: git.DefaultContext,
|
||||||
Metas: localMetas,
|
Metas: localMetas,
|
||||||
|
ContentMode: RenderContentAsComment,
|
||||||
})
|
})
|
||||||
|
|
||||||
class := "ref-issue"
|
class := "ref-issue"
|
||||||
|
@ -137,8 +139,9 @@ func TestRender_IssueIndexPattern2(t *testing.T) {
|
||||||
}
|
}
|
||||||
expectedNum := fmt.Sprintf(expectedFmt, links...)
|
expectedNum := fmt.Sprintf(expectedFmt, links...)
|
||||||
testRenderIssueIndexPattern(t, s, expectedNum, &RenderContext{
|
testRenderIssueIndexPattern(t, s, expectedNum, &RenderContext{
|
||||||
Ctx: git.DefaultContext,
|
Ctx: git.DefaultContext,
|
||||||
Metas: numericMetas,
|
Metas: numericMetas,
|
||||||
|
ContentMode: RenderContentAsComment,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -266,7 +269,6 @@ func TestRender_IssueIndexPattern_Document(t *testing.T) {
|
||||||
"user": "someUser",
|
"user": "someUser",
|
||||||
"repo": "someRepo",
|
"repo": "someRepo",
|
||||||
"style": IssueNameStyleNumeric,
|
"style": IssueNameStyleNumeric,
|
||||||
"mode": "document",
|
|
||||||
}
|
}
|
||||||
|
|
||||||
testRenderIssueIndexPattern(t, "#1", "#1", &RenderContext{
|
testRenderIssueIndexPattern(t, "#1", "#1", &RenderContext{
|
||||||
|
@ -316,8 +318,8 @@ func TestRender_AutoLink(t *testing.T) {
|
||||||
Links: Links{
|
Links: Links{
|
||||||
Base: TestRepoURL,
|
Base: TestRepoURL,
|
||||||
},
|
},
|
||||||
Metas: localMetas,
|
Metas: localMetas,
|
||||||
IsWiki: true,
|
ContentMode: RenderContentAsWiki,
|
||||||
}, strings.NewReader(input), &buffer)
|
}, strings.NewReader(input), &buffer)
|
||||||
assert.Equal(t, err, nil)
|
assert.Equal(t, err, nil)
|
||||||
assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(buffer.String()))
|
assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(buffer.String()))
|
||||||
|
@ -340,7 +342,7 @@ func TestRender_AutoLink(t *testing.T) {
|
||||||
|
|
||||||
func TestRender_FullIssueURLs(t *testing.T) {
|
func TestRender_FullIssueURLs(t *testing.T) {
|
||||||
setting.AppURL = TestAppURL
|
setting.AppURL = TestAppURL
|
||||||
|
defer testModule.MockVariableValue(&RenderBehaviorForTesting.DisableInternalAttributes, true)()
|
||||||
test := func(input, expected string) {
|
test := func(input, expected string) {
|
||||||
var result strings.Builder
|
var result strings.Builder
|
||||||
err := postProcess(&RenderContext{
|
err := postProcess(&RenderContext{
|
||||||
|
@ -351,9 +353,7 @@ func TestRender_FullIssueURLs(t *testing.T) {
|
||||||
Metas: localMetas,
|
Metas: localMetas,
|
||||||
}, []processor{fullIssuePatternProcessor}, strings.NewReader(input), &result)
|
}, []processor{fullIssuePatternProcessor}, strings.NewReader(input), &result)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
actual := result.String()
|
assert.Equal(t, expected, result.String())
|
||||||
actual = strings.ReplaceAll(actual, ` data-markdown-generated-content=""`, "")
|
|
||||||
assert.Equal(t, expected, actual)
|
|
||||||
}
|
}
|
||||||
test("Here is a link https://git.osgeo.org/gogs/postgis/postgis/pulls/6",
|
test("Here is a link https://git.osgeo.org/gogs/postgis/postgis/pulls/6",
|
||||||
"Here is a link https://git.osgeo.org/gogs/postgis/postgis/pulls/6")
|
"Here is a link https://git.osgeo.org/gogs/postgis/postgis/pulls/6")
|
||||||
|
|
|
@ -67,9 +67,8 @@ func issueIndexPatternProcessor(ctx *RenderContext, node *html.Node) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// FIXME: the use of "mode" is quite dirty and hacky, for example: what is a "document"? how should it be rendered?
|
// crossLinkOnly if not comment and not wiki
|
||||||
// The "mode" approach should be refactored to some other more clear&reliable way.
|
crossLinkOnly := ctx.ContentMode != RenderContentAsTitle && ctx.ContentMode != RenderContentAsComment && ctx.ContentMode != RenderContentAsWiki
|
||||||
crossLinkOnly := ctx.Metas["mode"] == "document" && !ctx.IsWiki
|
|
||||||
|
|
||||||
var (
|
var (
|
||||||
found bool
|
found bool
|
||||||
|
|
|
@ -20,7 +20,7 @@ func ResolveLink(ctx *RenderContext, link, userContentAnchorPrefix string) (resu
|
||||||
isAnchorFragment := link != "" && link[0] == '#'
|
isAnchorFragment := link != "" && link[0] == '#'
|
||||||
if !isAnchorFragment && !IsFullURLString(link) {
|
if !isAnchorFragment && !IsFullURLString(link) {
|
||||||
linkBase := ctx.Links.Base
|
linkBase := ctx.Links.Base
|
||||||
if ctx.IsWiki {
|
if ctx.ContentMode == RenderContentAsWiki {
|
||||||
// no need to check if the link should be resolved as a wiki link or a wiki raw link
|
// no need to check if the link should be resolved as a wiki link or a wiki raw link
|
||||||
// just use wiki link here and it will be redirected to a wiki raw link if necessary
|
// just use wiki link here and it will be redirected to a wiki raw link if necessary
|
||||||
linkBase = ctx.Links.WikiLink()
|
linkBase = ctx.Links.WikiLink()
|
||||||
|
@ -147,7 +147,7 @@ func shortLinkProcessor(ctx *RenderContext, node *html.Node) {
|
||||||
}
|
}
|
||||||
if image {
|
if image {
|
||||||
if !absoluteLink {
|
if !absoluteLink {
|
||||||
link = util.URLJoin(ctx.Links.ResolveMediaLink(ctx.IsWiki), link)
|
link = util.URLJoin(ctx.Links.ResolveMediaLink(ctx.ContentMode == RenderContentAsWiki), link)
|
||||||
}
|
}
|
||||||
title := props["title"]
|
title := props["title"]
|
||||||
if title == "" {
|
if title == "" {
|
||||||
|
|
|
@ -17,7 +17,7 @@ func visitNodeImg(ctx *RenderContext, img *html.Node) (next *html.Node) {
|
||||||
}
|
}
|
||||||
|
|
||||||
if IsNonEmptyRelativePath(attr.Val) {
|
if IsNonEmptyRelativePath(attr.Val) {
|
||||||
attr.Val = util.URLJoin(ctx.Links.ResolveMediaLink(ctx.IsWiki), attr.Val)
|
attr.Val = util.URLJoin(ctx.Links.ResolveMediaLink(ctx.ContentMode == RenderContentAsWiki), attr.Val)
|
||||||
|
|
||||||
// By default, the "<img>" tag should also be clickable,
|
// By default, the "<img>" tag should also be clickable,
|
||||||
// because frontend use `<img>` to paste the re-scaled image into the markdown,
|
// because frontend use `<img>` to paste the re-scaled image into the markdown,
|
||||||
|
@ -53,7 +53,7 @@ func visitNodeVideo(ctx *RenderContext, node *html.Node) (next *html.Node) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if IsNonEmptyRelativePath(attr.Val) {
|
if IsNonEmptyRelativePath(attr.Val) {
|
||||||
attr.Val = util.URLJoin(ctx.Links.ResolveMediaLink(ctx.IsWiki), attr.Val)
|
attr.Val = util.URLJoin(ctx.Links.ResolveMediaLink(ctx.ContentMode == RenderContentAsWiki), attr.Val)
|
||||||
}
|
}
|
||||||
attr.Val = camoHandleLink(attr.Val)
|
attr.Val = camoHandleLink(attr.Val)
|
||||||
node.Attr[i] = attr
|
node.Attr[i] = attr
|
||||||
|
|
|
@ -14,6 +14,7 @@ import (
|
||||||
"code.gitea.io/gitea/modules/markup"
|
"code.gitea.io/gitea/modules/markup"
|
||||||
"code.gitea.io/gitea/modules/markup/markdown"
|
"code.gitea.io/gitea/modules/markup/markdown"
|
||||||
"code.gitea.io/gitea/modules/setting"
|
"code.gitea.io/gitea/modules/setting"
|
||||||
|
testModule "code.gitea.io/gitea/modules/test"
|
||||||
"code.gitea.io/gitea/modules/util"
|
"code.gitea.io/gitea/modules/util"
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
|
@ -104,7 +105,7 @@ func TestRender_Commits(t *testing.T) {
|
||||||
|
|
||||||
func TestRender_CrossReferences(t *testing.T) {
|
func TestRender_CrossReferences(t *testing.T) {
|
||||||
setting.AppURL = markup.TestAppURL
|
setting.AppURL = markup.TestAppURL
|
||||||
|
defer testModule.MockVariableValue(&markup.RenderBehaviorForTesting.DisableInternalAttributes, true)()
|
||||||
test := func(input, expected string) {
|
test := func(input, expected string) {
|
||||||
buffer, err := markup.RenderString(&markup.RenderContext{
|
buffer, err := markup.RenderString(&markup.RenderContext{
|
||||||
Ctx: git.DefaultContext,
|
Ctx: git.DefaultContext,
|
||||||
|
@ -116,9 +117,7 @@ func TestRender_CrossReferences(t *testing.T) {
|
||||||
Metas: localMetas,
|
Metas: localMetas,
|
||||||
}, input)
|
}, input)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
actual := strings.TrimSpace(buffer)
|
assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(buffer))
|
||||||
actual = strings.ReplaceAll(actual, ` data-markdown-generated-content=""`, "")
|
|
||||||
assert.Equal(t, strings.TrimSpace(expected), actual)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
test(
|
test(
|
||||||
|
@ -148,7 +147,7 @@ func TestRender_CrossReferences(t *testing.T) {
|
||||||
|
|
||||||
func TestRender_links(t *testing.T) {
|
func TestRender_links(t *testing.T) {
|
||||||
setting.AppURL = markup.TestAppURL
|
setting.AppURL = markup.TestAppURL
|
||||||
|
defer testModule.MockVariableValue(&markup.RenderBehaviorForTesting.DisableInternalAttributes, true)()
|
||||||
test := func(input, expected string) {
|
test := func(input, expected string) {
|
||||||
buffer, err := markup.RenderString(&markup.RenderContext{
|
buffer, err := markup.RenderString(&markup.RenderContext{
|
||||||
Ctx: git.DefaultContext,
|
Ctx: git.DefaultContext,
|
||||||
|
@ -158,9 +157,7 @@ func TestRender_links(t *testing.T) {
|
||||||
},
|
},
|
||||||
}, input)
|
}, input)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
actual := strings.TrimSpace(buffer)
|
assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(buffer))
|
||||||
actual = strings.ReplaceAll(actual, ` data-markdown-generated-content=""`, "")
|
|
||||||
assert.Equal(t, strings.TrimSpace(expected), actual)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
oldCustomURLSchemes := setting.Markdown.CustomURLSchemes
|
oldCustomURLSchemes := setting.Markdown.CustomURLSchemes
|
||||||
|
@ -261,7 +258,7 @@ func TestRender_links(t *testing.T) {
|
||||||
|
|
||||||
func TestRender_email(t *testing.T) {
|
func TestRender_email(t *testing.T) {
|
||||||
setting.AppURL = markup.TestAppURL
|
setting.AppURL = markup.TestAppURL
|
||||||
|
defer testModule.MockVariableValue(&markup.RenderBehaviorForTesting.DisableInternalAttributes, true)()
|
||||||
test := func(input, expected string) {
|
test := func(input, expected string) {
|
||||||
res, err := markup.RenderString(&markup.RenderContext{
|
res, err := markup.RenderString(&markup.RenderContext{
|
||||||
Ctx: git.DefaultContext,
|
Ctx: git.DefaultContext,
|
||||||
|
@ -271,9 +268,7 @@ func TestRender_email(t *testing.T) {
|
||||||
},
|
},
|
||||||
}, input)
|
}, input)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
actual := strings.TrimSpace(res)
|
assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(res))
|
||||||
actual = strings.ReplaceAll(actual, ` data-markdown-generated-content=""`, "")
|
|
||||||
assert.Equal(t, strings.TrimSpace(expected), actual)
|
|
||||||
}
|
}
|
||||||
// Text that should be turned into email link
|
// Text that should be turned into email link
|
||||||
|
|
||||||
|
@ -302,10 +297,10 @@ func TestRender_email(t *testing.T) {
|
||||||
j.doe@example.com;
|
j.doe@example.com;
|
||||||
j.doe@example.com?
|
j.doe@example.com?
|
||||||
j.doe@example.com!`,
|
j.doe@example.com!`,
|
||||||
`<p><a href="mailto:j.doe@example.com" rel="nofollow">j.doe@example.com</a>,<br/>
|
`<p><a href="mailto:j.doe@example.com" rel="nofollow">j.doe@example.com</a>,
|
||||||
<a href="mailto:j.doe@example.com" rel="nofollow">j.doe@example.com</a>.<br/>
|
<a href="mailto:j.doe@example.com" rel="nofollow">j.doe@example.com</a>.
|
||||||
<a href="mailto:j.doe@example.com" rel="nofollow">j.doe@example.com</a>;<br/>
|
<a href="mailto:j.doe@example.com" rel="nofollow">j.doe@example.com</a>;
|
||||||
<a href="mailto:j.doe@example.com" rel="nofollow">j.doe@example.com</a>?<br/>
|
<a href="mailto:j.doe@example.com" rel="nofollow">j.doe@example.com</a>?
|
||||||
<a href="mailto:j.doe@example.com" rel="nofollow">j.doe@example.com</a>!</p>`)
|
<a href="mailto:j.doe@example.com" rel="nofollow">j.doe@example.com</a>!</p>`)
|
||||||
|
|
||||||
// Test that should *not* be turned into email links
|
// Test that should *not* be turned into email links
|
||||||
|
@ -418,8 +413,8 @@ func TestRender_ShortLinks(t *testing.T) {
|
||||||
Links: markup.Links{
|
Links: markup.Links{
|
||||||
Base: markup.TestRepoURL,
|
Base: markup.TestRepoURL,
|
||||||
},
|
},
|
||||||
Metas: localMetas,
|
Metas: localMetas,
|
||||||
IsWiki: true,
|
ContentMode: markup.RenderContentAsWiki,
|
||||||
}, input)
|
}, input)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
assert.Equal(t, strings.TrimSpace(expectedWiki), strings.TrimSpace(string(buffer)))
|
assert.Equal(t, strings.TrimSpace(expectedWiki), strings.TrimSpace(string(buffer)))
|
||||||
|
@ -531,10 +526,10 @@ func TestRender_ShortLinks(t *testing.T) {
|
||||||
func TestRender_RelativeMedias(t *testing.T) {
|
func TestRender_RelativeMedias(t *testing.T) {
|
||||||
render := func(input string, isWiki bool, links markup.Links) string {
|
render := func(input string, isWiki bool, links markup.Links) string {
|
||||||
buffer, err := markdown.RenderString(&markup.RenderContext{
|
buffer, err := markdown.RenderString(&markup.RenderContext{
|
||||||
Ctx: git.DefaultContext,
|
Ctx: git.DefaultContext,
|
||||||
Links: links,
|
Links: links,
|
||||||
Metas: localMetas,
|
Metas: localMetas,
|
||||||
IsWiki: isWiki,
|
ContentMode: util.Iif(isWiki, markup.RenderContentAsWiki, markup.RenderContentAsComment),
|
||||||
}, input)
|
}, input)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
return strings.TrimSpace(string(buffer))
|
return strings.TrimSpace(string(buffer))
|
||||||
|
@ -604,12 +599,7 @@ func Test_ParseClusterFuzz(t *testing.T) {
|
||||||
func TestPostProcess_RenderDocument(t *testing.T) {
|
func TestPostProcess_RenderDocument(t *testing.T) {
|
||||||
setting.AppURL = markup.TestAppURL
|
setting.AppURL = markup.TestAppURL
|
||||||
setting.StaticURLPrefix = markup.TestAppURL // can't run standalone
|
setting.StaticURLPrefix = markup.TestAppURL // can't run standalone
|
||||||
|
defer testModule.MockVariableValue(&markup.RenderBehaviorForTesting.DisableInternalAttributes, true)()
|
||||||
localMetas := map[string]string{
|
|
||||||
"user": "go-gitea",
|
|
||||||
"repo": "gitea",
|
|
||||||
"mode": "document",
|
|
||||||
}
|
|
||||||
|
|
||||||
test := func(input, expected string) {
|
test := func(input, expected string) {
|
||||||
var res strings.Builder
|
var res strings.Builder
|
||||||
|
@ -619,12 +609,10 @@ func TestPostProcess_RenderDocument(t *testing.T) {
|
||||||
AbsolutePrefix: true,
|
AbsolutePrefix: true,
|
||||||
Base: "https://example.com",
|
Base: "https://example.com",
|
||||||
},
|
},
|
||||||
Metas: localMetas,
|
Metas: map[string]string{"user": "go-gitea", "repo": "gitea"},
|
||||||
}, strings.NewReader(input), &res)
|
}, strings.NewReader(input), &res)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
actual := strings.TrimSpace(res.String())
|
assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(res.String()))
|
||||||
actual = strings.ReplaceAll(actual, ` data-markdown-generated-content=""`, "")
|
|
||||||
assert.Equal(t, strings.TrimSpace(expected), actual)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Issue index shouldn't be post processing in a document.
|
// Issue index shouldn't be post processing in a document.
|
||||||
|
|
|
@ -72,7 +72,12 @@ func (g *ASTTransformer) Transform(node *ast.Document, reader text.Reader, pc pa
|
||||||
g.transformList(ctx, v, rc)
|
g.transformList(ctx, v, rc)
|
||||||
case *ast.Text:
|
case *ast.Text:
|
||||||
if v.SoftLineBreak() && !v.HardLineBreak() {
|
if v.SoftLineBreak() && !v.HardLineBreak() {
|
||||||
if ctx.Metas["mode"] != "document" {
|
// TODO: this was a quite unclear part, old code: `if metas["mode"] != "document" { use comment link break setting }`
|
||||||
|
// many places render non-comment contents with no mode=document, then these contents also use comment's hard line break setting
|
||||||
|
// especially in many tests.
|
||||||
|
if markup.RenderBehaviorForTesting.ForceHardLineBreak {
|
||||||
|
v.SetHardLineBreak(true)
|
||||||
|
} else if ctx.ContentMode == markup.RenderContentAsComment {
|
||||||
v.SetHardLineBreak(setting.Markdown.EnableHardLineBreakInComments)
|
v.SetHardLineBreak(setting.Markdown.EnableHardLineBreakInComments)
|
||||||
} else {
|
} else {
|
||||||
v.SetHardLineBreak(setting.Markdown.EnableHardLineBreakInDocuments)
|
v.SetHardLineBreak(setting.Markdown.EnableHardLineBreakInDocuments)
|
||||||
|
|
|
@ -257,9 +257,7 @@ func (Renderer) Render(ctx *markup.RenderContext, input io.Reader, output io.Wri
|
||||||
|
|
||||||
// Render renders Markdown to HTML with all specific handling stuff.
|
// Render renders Markdown to HTML with all specific handling stuff.
|
||||||
func Render(ctx *markup.RenderContext, input io.Reader, output io.Writer) error {
|
func Render(ctx *markup.RenderContext, input io.Reader, output io.Writer) error {
|
||||||
if ctx.Type == "" {
|
ctx.MarkupType = MarkupName
|
||||||
ctx.Type = MarkupName
|
|
||||||
}
|
|
||||||
return markup.Render(ctx, input, output)
|
return markup.Render(ctx, input, output)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -16,6 +16,7 @@ import (
|
||||||
"code.gitea.io/gitea/modules/markup/markdown"
|
"code.gitea.io/gitea/modules/markup/markdown"
|
||||||
"code.gitea.io/gitea/modules/setting"
|
"code.gitea.io/gitea/modules/setting"
|
||||||
"code.gitea.io/gitea/modules/svg"
|
"code.gitea.io/gitea/modules/svg"
|
||||||
|
"code.gitea.io/gitea/modules/test"
|
||||||
"code.gitea.io/gitea/modules/util"
|
"code.gitea.io/gitea/modules/util"
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
|
@ -74,7 +75,7 @@ func TestRender_StandardLinks(t *testing.T) {
|
||||||
Links: markup.Links{
|
Links: markup.Links{
|
||||||
Base: FullURL,
|
Base: FullURL,
|
||||||
},
|
},
|
||||||
IsWiki: true,
|
ContentMode: markup.RenderContentAsWiki,
|
||||||
}, input)
|
}, input)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
assert.Equal(t, strings.TrimSpace(expectedWiki), strings.TrimSpace(string(buffer)))
|
assert.Equal(t, strings.TrimSpace(expectedWiki), strings.TrimSpace(string(buffer)))
|
||||||
|
@ -296,23 +297,22 @@ This PR has been generated by [Renovate Bot](https://github.com/renovatebot/reno
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestTotal_RenderWiki(t *testing.T) {
|
func TestTotal_RenderWiki(t *testing.T) {
|
||||||
|
defer test.MockVariableValue(&markup.RenderBehaviorForTesting.ForceHardLineBreak, true)()
|
||||||
|
defer test.MockVariableValue(&markup.RenderBehaviorForTesting.DisableInternalAttributes, true)()
|
||||||
setting.AppURL = AppURL
|
setting.AppURL = AppURL
|
||||||
|
|
||||||
answers := testAnswers(util.URLJoin(FullURL, "wiki"), util.URLJoin(FullURL, "wiki", "raw"))
|
answers := testAnswers(util.URLJoin(FullURL, "wiki"), util.URLJoin(FullURL, "wiki", "raw"))
|
||||||
|
|
||||||
for i := 0; i < len(sameCases); i++ {
|
for i := 0; i < len(sameCases); i++ {
|
||||||
line, err := markdown.RenderString(&markup.RenderContext{
|
line, err := markdown.RenderString(&markup.RenderContext{
|
||||||
Ctx: git.DefaultContext,
|
Ctx: git.DefaultContext,
|
||||||
Links: markup.Links{
|
Links: markup.Links{
|
||||||
Base: FullURL,
|
Base: FullURL,
|
||||||
},
|
},
|
||||||
Repo: newMockRepo(testRepoOwnerName, testRepoName),
|
Repo: newMockRepo(testRepoOwnerName, testRepoName),
|
||||||
Metas: localMetas,
|
Metas: localMetas,
|
||||||
IsWiki: true,
|
ContentMode: markup.RenderContentAsWiki,
|
||||||
}, sameCases[i])
|
}, sameCases[i])
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
actual := strings.ReplaceAll(string(line), ` data-markdown-generated-content=""`, "")
|
assert.Equal(t, answers[i], string(line))
|
||||||
assert.Equal(t, answers[i], actual)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
testCases := []string{
|
testCases := []string{
|
||||||
|
@ -334,19 +334,18 @@ func TestTotal_RenderWiki(t *testing.T) {
|
||||||
Links: markup.Links{
|
Links: markup.Links{
|
||||||
Base: FullURL,
|
Base: FullURL,
|
||||||
},
|
},
|
||||||
IsWiki: true,
|
ContentMode: markup.RenderContentAsWiki,
|
||||||
}, testCases[i])
|
}, testCases[i])
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
actual := strings.ReplaceAll(string(line), ` data-markdown-generated-content=""`, "")
|
assert.EqualValues(t, testCases[i+1], string(line))
|
||||||
assert.EqualValues(t, testCases[i+1], actual)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestTotal_RenderString(t *testing.T) {
|
func TestTotal_RenderString(t *testing.T) {
|
||||||
|
defer test.MockVariableValue(&markup.RenderBehaviorForTesting.ForceHardLineBreak, true)()
|
||||||
|
defer test.MockVariableValue(&markup.RenderBehaviorForTesting.DisableInternalAttributes, true)()
|
||||||
setting.AppURL = AppURL
|
setting.AppURL = AppURL
|
||||||
|
|
||||||
answers := testAnswers(util.URLJoin(FullURL, "src", "master"), util.URLJoin(FullURL, "media", "master"))
|
answers := testAnswers(util.URLJoin(FullURL, "src", "master"), util.URLJoin(FullURL, "media", "master"))
|
||||||
|
|
||||||
for i := 0; i < len(sameCases); i++ {
|
for i := 0; i < len(sameCases); i++ {
|
||||||
line, err := markdown.RenderString(&markup.RenderContext{
|
line, err := markdown.RenderString(&markup.RenderContext{
|
||||||
Ctx: git.DefaultContext,
|
Ctx: git.DefaultContext,
|
||||||
|
@ -358,8 +357,7 @@ func TestTotal_RenderString(t *testing.T) {
|
||||||
Metas: localMetas,
|
Metas: localMetas,
|
||||||
}, sameCases[i])
|
}, sameCases[i])
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
actual := strings.ReplaceAll(string(line), ` data-markdown-generated-content=""`, "")
|
assert.Equal(t, answers[i], string(line))
|
||||||
assert.Equal(t, answers[i], actual)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
testCases := []string{}
|
testCases := []string{}
|
||||||
|
@ -428,6 +426,7 @@ func TestRenderSiblingImages_Issue12925(t *testing.T) {
|
||||||
expected := `<p><a href="/image1" target="_blank" rel="nofollow noopener"><img src="/image1" alt="image1"></a><br>
|
expected := `<p><a href="/image1" target="_blank" rel="nofollow noopener"><img src="/image1" alt="image1"></a><br>
|
||||||
<a href="/image2" target="_blank" rel="nofollow noopener"><img src="/image2" alt="image2"></a></p>
|
<a href="/image2" target="_blank" rel="nofollow noopener"><img src="/image2" alt="image2"></a></p>
|
||||||
`
|
`
|
||||||
|
defer test.MockVariableValue(&markup.RenderBehaviorForTesting.ForceHardLineBreak, true)()
|
||||||
res, err := markdown.RenderRawString(&markup.RenderContext{Ctx: git.DefaultContext}, testcase)
|
res, err := markdown.RenderRawString(&markup.RenderContext{Ctx: git.DefaultContext}, testcase)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
assert.Equal(t, expected, res)
|
assert.Equal(t, expected, res)
|
||||||
|
@ -996,11 +995,16 @@ space</p>
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
defer test.MockVariableValue(&markup.RenderBehaviorForTesting.ForceHardLineBreak, true)()
|
||||||
|
defer test.MockVariableValue(&markup.RenderBehaviorForTesting.DisableInternalAttributes, true)()
|
||||||
for i, c := range cases {
|
for i, c := range cases {
|
||||||
result, err := markdown.RenderString(&markup.RenderContext{Ctx: context.Background(), Links: c.Links, IsWiki: c.IsWiki}, input)
|
result, err := markdown.RenderString(&markup.RenderContext{
|
||||||
|
Ctx: context.Background(),
|
||||||
|
Links: c.Links,
|
||||||
|
ContentMode: util.Iif(c.IsWiki, markup.RenderContentAsWiki, markup.RenderContentAsDefault),
|
||||||
|
}, input)
|
||||||
assert.NoError(t, err, "Unexpected error in testcase: %v", i)
|
assert.NoError(t, err, "Unexpected error in testcase: %v", i)
|
||||||
actual := strings.ReplaceAll(string(result), ` data-markdown-generated-content=""`, "")
|
assert.Equal(t, c.Expected, string(result), "Unexpected result in testcase %v", i)
|
||||||
assert.Equal(t, c.Expected, actual, "Unexpected result in testcase %v", i)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -21,7 +21,7 @@ func (g *ASTTransformer) transformImage(ctx *markup.RenderContext, v *ast.Image)
|
||||||
// Check if the destination is a real link
|
// Check if the destination is a real link
|
||||||
if len(v.Destination) > 0 && !markup.IsFullURLBytes(v.Destination) {
|
if len(v.Destination) > 0 && !markup.IsFullURLBytes(v.Destination) {
|
||||||
v.Destination = []byte(giteautil.URLJoin(
|
v.Destination = []byte(giteautil.URLJoin(
|
||||||
ctx.Links.ResolveMediaLink(ctx.IsWiki),
|
ctx.Links.ResolveMediaLink(ctx.ContentMode == markup.RenderContentAsWiki),
|
||||||
strings.TrimLeft(string(v.Destination), "/"),
|
strings.TrimLeft(string(v.Destination), "/"),
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
|
@ -144,14 +144,15 @@ func (r *Writer) resolveLink(kind, link string) string {
|
||||||
}
|
}
|
||||||
|
|
||||||
base := r.Ctx.Links.Base
|
base := r.Ctx.Links.Base
|
||||||
if r.Ctx.IsWiki {
|
isWiki := r.Ctx.ContentMode == markup.RenderContentAsWiki
|
||||||
|
if isWiki {
|
||||||
base = r.Ctx.Links.WikiLink()
|
base = r.Ctx.Links.WikiLink()
|
||||||
} else if r.Ctx.Links.HasBranchInfo() {
|
} else if r.Ctx.Links.HasBranchInfo() {
|
||||||
base = r.Ctx.Links.SrcLink()
|
base = r.Ctx.Links.SrcLink()
|
||||||
}
|
}
|
||||||
|
|
||||||
if kind == "image" || kind == "video" {
|
if kind == "image" || kind == "video" {
|
||||||
base = r.Ctx.Links.ResolveMediaLink(r.Ctx.IsWiki)
|
base = r.Ctx.Links.ResolveMediaLink(isWiki)
|
||||||
}
|
}
|
||||||
|
|
||||||
link = util.URLJoin(base, link)
|
link = util.URLJoin(base, link)
|
||||||
|
|
|
@ -10,6 +10,7 @@ import (
|
||||||
"code.gitea.io/gitea/modules/git"
|
"code.gitea.io/gitea/modules/git"
|
||||||
"code.gitea.io/gitea/modules/markup"
|
"code.gitea.io/gitea/modules/markup"
|
||||||
"code.gitea.io/gitea/modules/setting"
|
"code.gitea.io/gitea/modules/setting"
|
||||||
|
"code.gitea.io/gitea/modules/util"
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
)
|
)
|
||||||
|
@ -26,7 +27,7 @@ func TestRender_StandardLinks(t *testing.T) {
|
||||||
Base: "/relative-path",
|
Base: "/relative-path",
|
||||||
BranchPath: "branch/main",
|
BranchPath: "branch/main",
|
||||||
},
|
},
|
||||||
IsWiki: isWiki,
|
ContentMode: util.Iif(isWiki, markup.RenderContentAsWiki, markup.RenderContentAsDefault),
|
||||||
}, input)
|
}, input)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(buffer))
|
assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(buffer))
|
||||||
|
|
|
@ -5,11 +5,9 @@ package markup
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"errors"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"net/url"
|
"net/url"
|
||||||
"path/filepath"
|
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
|
@ -29,15 +27,44 @@ const (
|
||||||
RenderMetaAsTable RenderMetaMode = "table"
|
RenderMetaAsTable RenderMetaMode = "table"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type RenderContentMode string
|
||||||
|
|
||||||
|
const (
|
||||||
|
RenderContentAsDefault RenderContentMode = "" // empty means "default", no special handling, maybe just a simple "document"
|
||||||
|
RenderContentAsComment RenderContentMode = "comment"
|
||||||
|
RenderContentAsTitle RenderContentMode = "title"
|
||||||
|
RenderContentAsWiki RenderContentMode = "wiki"
|
||||||
|
)
|
||||||
|
|
||||||
|
var RenderBehaviorForTesting struct {
|
||||||
|
// Markdown line break rendering has 2 default behaviors:
|
||||||
|
// * Use hard: replace "\n" with "<br>" for comments, setting.Markdown.EnableHardLineBreakInComments=true
|
||||||
|
// * Keep soft: "\n" for non-comments (a.k.a. documents), setting.Markdown.EnableHardLineBreakInDocuments=false
|
||||||
|
// In history, there was a mess:
|
||||||
|
// * The behavior was controlled by `Metas["mode"] != "document",
|
||||||
|
// * However, many places render the content without setting "mode" in Metas, all these places used comment line break setting incorrectly
|
||||||
|
ForceHardLineBreak bool
|
||||||
|
|
||||||
|
// Gitea will emit some internal attributes for various purposes, these attributes don't affect rendering.
|
||||||
|
// But there are too many hard-coded test cases, to avoid changing all of them again and again, we can disable emitting these internal attributes.
|
||||||
|
DisableInternalAttributes bool
|
||||||
|
}
|
||||||
|
|
||||||
// RenderContext represents a render context
|
// RenderContext represents a render context
|
||||||
type RenderContext struct {
|
type RenderContext struct {
|
||||||
Ctx context.Context
|
Ctx context.Context
|
||||||
RelativePath string // relative path from tree root of the branch
|
RelativePath string // relative path from tree root of the branch
|
||||||
Type string
|
|
||||||
IsWiki bool
|
// eg: "orgmode", "asciicast", "console"
|
||||||
Links Links
|
// for file mode, it could be left as empty, and will be detected by file extension in RelativePath
|
||||||
Metas map[string]string // user, repo, mode(comment/document)
|
MarkupType string
|
||||||
DefaultLink string
|
|
||||||
|
// what the content will be used for: eg: for comment or for wiki? or just render a file?
|
||||||
|
ContentMode RenderContentMode
|
||||||
|
|
||||||
|
Links Links // special link references for rendering, especially when there is a branch/tree path
|
||||||
|
Metas map[string]string // user&repo, format&style®exp (for external issue pattern), teams&org (for mention), BranchNameSubURL(for iframe&asciicast)
|
||||||
|
DefaultLink string // TODO: need to figure out
|
||||||
GitRepo *git.Repository
|
GitRepo *git.Repository
|
||||||
Repo gitrepo.Repository
|
Repo gitrepo.Repository
|
||||||
ShaExistCache map[string]bool
|
ShaExistCache map[string]bool
|
||||||
|
@ -77,12 +104,29 @@ func (ctx *RenderContext) AddCancel(fn func()) {
|
||||||
|
|
||||||
// Render renders markup file to HTML with all specific handling stuff.
|
// Render renders markup file to HTML with all specific handling stuff.
|
||||||
func Render(ctx *RenderContext, input io.Reader, output io.Writer) error {
|
func Render(ctx *RenderContext, input io.Reader, output io.Writer) error {
|
||||||
if ctx.Type != "" {
|
if ctx.MarkupType == "" && ctx.RelativePath != "" {
|
||||||
return renderByType(ctx, input, output)
|
ctx.MarkupType = DetectMarkupTypeByFileName(ctx.RelativePath)
|
||||||
} else if ctx.RelativePath != "" {
|
if ctx.MarkupType == "" {
|
||||||
return renderFile(ctx, input, output)
|
return util.NewInvalidArgumentErrorf("unsupported file to render: %q", ctx.RelativePath)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return errors.New("render options both filename and type missing")
|
|
||||||
|
renderer := renderers[ctx.MarkupType]
|
||||||
|
if renderer == nil {
|
||||||
|
return util.NewInvalidArgumentErrorf("unsupported markup type: %q", ctx.MarkupType)
|
||||||
|
}
|
||||||
|
|
||||||
|
if ctx.RelativePath != "" {
|
||||||
|
if externalRender, ok := renderer.(ExternalRenderer); ok && externalRender.DisplayInIFrame() {
|
||||||
|
if !ctx.InStandalonePage {
|
||||||
|
// for an external "DisplayInIFrame" render, it could only output its content in a standalone page
|
||||||
|
// otherwise, a <iframe> should be outputted to embed the external rendered page
|
||||||
|
return renderIFrame(ctx, output)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return render(ctx, renderer, input, output)
|
||||||
}
|
}
|
||||||
|
|
||||||
// RenderString renders Markup string to HTML with all specific handling stuff and return string
|
// RenderString renders Markup string to HTML with all specific handling stuff and return string
|
||||||
|
@ -170,42 +214,6 @@ func render(ctx *RenderContext, renderer Renderer, input io.Reader, output io.Wr
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
func renderByType(ctx *RenderContext, input io.Reader, output io.Writer) error {
|
|
||||||
if renderer, ok := renderers[ctx.Type]; ok {
|
|
||||||
return render(ctx, renderer, input, output)
|
|
||||||
}
|
|
||||||
return fmt.Errorf("unsupported render type: %s", ctx.Type)
|
|
||||||
}
|
|
||||||
|
|
||||||
// ErrUnsupportedRenderExtension represents the error when extension doesn't supported to render
|
|
||||||
type ErrUnsupportedRenderExtension struct {
|
|
||||||
Extension string
|
|
||||||
}
|
|
||||||
|
|
||||||
func IsErrUnsupportedRenderExtension(err error) bool {
|
|
||||||
_, ok := err.(ErrUnsupportedRenderExtension)
|
|
||||||
return ok
|
|
||||||
}
|
|
||||||
|
|
||||||
func (err ErrUnsupportedRenderExtension) Error() string {
|
|
||||||
return fmt.Sprintf("Unsupported render extension: %s", err.Extension)
|
|
||||||
}
|
|
||||||
|
|
||||||
func renderFile(ctx *RenderContext, input io.Reader, output io.Writer) error {
|
|
||||||
extension := strings.ToLower(filepath.Ext(ctx.RelativePath))
|
|
||||||
if renderer, ok := extRenderers[extension]; ok {
|
|
||||||
if r, ok := renderer.(ExternalRenderer); ok && r.DisplayInIFrame() {
|
|
||||||
if !ctx.InStandalonePage {
|
|
||||||
// for an external render, it could only output its content in a standalone page
|
|
||||||
// otherwise, a <iframe> should be outputted to embed the external rendered page
|
|
||||||
return renderIFrame(ctx, output)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return render(ctx, renderer, input, output)
|
|
||||||
}
|
|
||||||
return ErrUnsupportedRenderExtension{extension}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Init initializes the render global variables
|
// Init initializes the render global variables
|
||||||
func Init(ph *ProcessorHelper) {
|
func Init(ph *ProcessorHelper) {
|
||||||
if ph != nil {
|
if ph != nil {
|
||||||
|
|
|
@ -21,7 +21,7 @@ type MarkupOption struct {
|
||||||
//
|
//
|
||||||
// in: body
|
// in: body
|
||||||
Text string
|
Text string
|
||||||
// Mode to render (comment, gfm, markdown, file)
|
// Mode to render (markdown, comment, wiki, file)
|
||||||
//
|
//
|
||||||
// in: body
|
// in: body
|
||||||
Mode string
|
Mode string
|
||||||
|
@ -30,8 +30,9 @@ type MarkupOption struct {
|
||||||
//
|
//
|
||||||
// in: body
|
// in: body
|
||||||
Context string
|
Context string
|
||||||
// Is it a wiki page ?
|
// Is it a wiki page? (use mode=wiki instead)
|
||||||
//
|
//
|
||||||
|
// Deprecated: true
|
||||||
// in: body
|
// in: body
|
||||||
Wiki bool
|
Wiki bool
|
||||||
// File path for detecting extension in file mode
|
// File path for detecting extension in file mode
|
||||||
|
@ -50,7 +51,7 @@ type MarkdownOption struct {
|
||||||
//
|
//
|
||||||
// in: body
|
// in: body
|
||||||
Text string
|
Text string
|
||||||
// Mode to render (comment, gfm, markdown)
|
// Mode to render (markdown, comment, wiki, file)
|
||||||
//
|
//
|
||||||
// in: body
|
// in: body
|
||||||
Mode string
|
Mode string
|
||||||
|
@ -59,8 +60,9 @@ type MarkdownOption struct {
|
||||||
//
|
//
|
||||||
// in: body
|
// in: body
|
||||||
Context string
|
Context string
|
||||||
// Is it a wiki page ?
|
// Is it a wiki page? (use mode=wiki instead)
|
||||||
//
|
//
|
||||||
|
// Deprecated: true
|
||||||
// in: body
|
// in: body
|
||||||
Wiki bool
|
Wiki bool
|
||||||
}
|
}
|
||||||
|
|
|
@ -94,8 +94,9 @@ func (ut *RenderUtils) RenderCommitBody(msg string, metas map[string]string) tem
|
||||||
}
|
}
|
||||||
|
|
||||||
renderedMessage, err := markup.RenderCommitMessage(&markup.RenderContext{
|
renderedMessage, err := markup.RenderCommitMessage(&markup.RenderContext{
|
||||||
Ctx: ut.ctx,
|
Ctx: ut.ctx,
|
||||||
Metas: metas,
|
Metas: metas,
|
||||||
|
ContentMode: markup.RenderContentAsComment,
|
||||||
}, template.HTMLEscapeString(msgLine))
|
}, template.HTMLEscapeString(msgLine))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error("RenderCommitMessage: %v", err)
|
log.Error("RenderCommitMessage: %v", err)
|
||||||
|
@ -116,8 +117,9 @@ func renderCodeBlock(htmlEscapedTextToRender template.HTML) template.HTML {
|
||||||
// RenderIssueTitle renders issue/pull title with defined post processors
|
// RenderIssueTitle renders issue/pull title with defined post processors
|
||||||
func (ut *RenderUtils) RenderIssueTitle(text string, metas map[string]string) template.HTML {
|
func (ut *RenderUtils) RenderIssueTitle(text string, metas map[string]string) template.HTML {
|
||||||
renderedText, err := markup.RenderIssueTitle(&markup.RenderContext{
|
renderedText, err := markup.RenderIssueTitle(&markup.RenderContext{
|
||||||
Ctx: ut.ctx,
|
Ctx: ut.ctx,
|
||||||
Metas: metas,
|
ContentMode: markup.RenderContentAsTitle,
|
||||||
|
Metas: metas,
|
||||||
}, template.HTMLEscapeString(text))
|
}, template.HTMLEscapeString(text))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error("RenderIssueTitle: %v", err)
|
log.Error("RenderIssueTitle: %v", err)
|
||||||
|
|
|
@ -15,6 +15,7 @@ import (
|
||||||
"code.gitea.io/gitea/modules/git"
|
"code.gitea.io/gitea/modules/git"
|
||||||
"code.gitea.io/gitea/modules/log"
|
"code.gitea.io/gitea/modules/log"
|
||||||
"code.gitea.io/gitea/modules/markup"
|
"code.gitea.io/gitea/modules/markup"
|
||||||
|
"code.gitea.io/gitea/modules/test"
|
||||||
"code.gitea.io/gitea/modules/translation"
|
"code.gitea.io/gitea/modules/translation"
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
|
@ -72,6 +73,7 @@ func newTestRenderUtils() *RenderUtils {
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestRenderCommitBody(t *testing.T) {
|
func TestRenderCommitBody(t *testing.T) {
|
||||||
|
defer test.MockVariableValue(&markup.RenderBehaviorForTesting.DisableInternalAttributes, true)()
|
||||||
type args struct {
|
type args struct {
|
||||||
msg string
|
msg string
|
||||||
metas map[string]string
|
metas map[string]string
|
||||||
|
@ -129,23 +131,21 @@ com 88fc37a3c0a4dda553bdcfc80c178a58247f42fb mit
|
||||||
<a href="/mention-user" class="mention">@mention-user</a> test
|
<a href="/mention-user" class="mention">@mention-user</a> test
|
||||||
<a href="/user13/repo11/issues/123" class="ref-issue">#123</a>
|
<a href="/user13/repo11/issues/123" class="ref-issue">#123</a>
|
||||||
space`
|
space`
|
||||||
actual := strings.ReplaceAll(string(newTestRenderUtils().RenderCommitBody(testInput(), testMetas)), ` data-markdown-generated-content=""`, "")
|
assert.EqualValues(t, expected, string(newTestRenderUtils().RenderCommitBody(testInput(), testMetas)))
|
||||||
assert.EqualValues(t, expected, actual)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestRenderCommitMessage(t *testing.T) {
|
func TestRenderCommitMessage(t *testing.T) {
|
||||||
expected := `space <a href="/mention-user" data-markdown-generated-content="" class="mention">@mention-user</a> `
|
expected := `space <a href="/mention-user" data-markdown-generated-content="" class="mention">@mention-user</a> `
|
||||||
|
|
||||||
assert.EqualValues(t, expected, newTestRenderUtils().RenderCommitMessage(testInput(), testMetas))
|
assert.EqualValues(t, expected, newTestRenderUtils().RenderCommitMessage(testInput(), testMetas))
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestRenderCommitMessageLinkSubject(t *testing.T) {
|
func TestRenderCommitMessageLinkSubject(t *testing.T) {
|
||||||
expected := `<a href="https://example.com/link" class="default-link muted">space </a><a href="/mention-user" data-markdown-generated-content="" class="mention">@mention-user</a>`
|
expected := `<a href="https://example.com/link" class="default-link muted">space </a><a href="/mention-user" data-markdown-generated-content="" class="mention">@mention-user</a>`
|
||||||
|
|
||||||
assert.EqualValues(t, expected, newTestRenderUtils().RenderCommitMessageLinkSubject(testInput(), "https://example.com/link", testMetas))
|
assert.EqualValues(t, expected, newTestRenderUtils().RenderCommitMessageLinkSubject(testInput(), "https://example.com/link", testMetas))
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestRenderIssueTitle(t *testing.T) {
|
func TestRenderIssueTitle(t *testing.T) {
|
||||||
|
defer test.MockVariableValue(&markup.RenderBehaviorForTesting.DisableInternalAttributes, true)()
|
||||||
expected := ` space @mention-user<SPACE><SPACE>
|
expected := ` space @mention-user<SPACE><SPACE>
|
||||||
/just/a/path.bin
|
/just/a/path.bin
|
||||||
https://example.com/file.bin
|
https://example.com/file.bin
|
||||||
|
@ -168,11 +168,11 @@ mail@domain.com
|
||||||
space<SPACE><SPACE>
|
space<SPACE><SPACE>
|
||||||
`
|
`
|
||||||
expected = strings.ReplaceAll(expected, "<SPACE>", " ")
|
expected = strings.ReplaceAll(expected, "<SPACE>", " ")
|
||||||
actual := strings.ReplaceAll(string(newTestRenderUtils().RenderIssueTitle(testInput(), testMetas)), ` data-markdown-generated-content=""`, "")
|
assert.EqualValues(t, expected, string(newTestRenderUtils().RenderIssueTitle(testInput(), testMetas)))
|
||||||
assert.EqualValues(t, expected, actual)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestRenderMarkdownToHtml(t *testing.T) {
|
func TestRenderMarkdownToHtml(t *testing.T) {
|
||||||
|
defer test.MockVariableValue(&markup.RenderBehaviorForTesting.DisableInternalAttributes, true)()
|
||||||
expected := `<p>space <a href="/mention-user" rel="nofollow">@mention-user</a><br/>
|
expected := `<p>space <a href="/mention-user" rel="nofollow">@mention-user</a><br/>
|
||||||
/just/a/path.bin
|
/just/a/path.bin
|
||||||
<a href="https://example.com/file.bin" rel="nofollow">https://example.com/file.bin</a>
|
<a href="https://example.com/file.bin" rel="nofollow">https://example.com/file.bin</a>
|
||||||
|
@ -194,8 +194,7 @@ com 88fc37a3c0a4dda553bdcfc80c178a58247f42fb mit
|
||||||
#123
|
#123
|
||||||
space</p>
|
space</p>
|
||||||
`
|
`
|
||||||
actual := strings.ReplaceAll(string(newTestRenderUtils().MarkdownToHtml(testInput())), ` data-markdown-generated-content=""`, "")
|
assert.Equal(t, expected, string(newTestRenderUtils().MarkdownToHtml(testInput())))
|
||||||
assert.Equal(t, expected, actual)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestRenderLabels(t *testing.T) {
|
func TestRenderLabels(t *testing.T) {
|
||||||
|
|
|
@ -9,6 +9,7 @@ import (
|
||||||
"code.gitea.io/gitea/modules/markup"
|
"code.gitea.io/gitea/modules/markup"
|
||||||
"code.gitea.io/gitea/modules/markup/markdown"
|
"code.gitea.io/gitea/modules/markup/markdown"
|
||||||
api "code.gitea.io/gitea/modules/structs"
|
api "code.gitea.io/gitea/modules/structs"
|
||||||
|
"code.gitea.io/gitea/modules/util"
|
||||||
"code.gitea.io/gitea/modules/web"
|
"code.gitea.io/gitea/modules/web"
|
||||||
"code.gitea.io/gitea/routers/common"
|
"code.gitea.io/gitea/routers/common"
|
||||||
"code.gitea.io/gitea/services/context"
|
"code.gitea.io/gitea/services/context"
|
||||||
|
@ -41,7 +42,8 @@ func Markup(ctx *context.APIContext) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
common.RenderMarkup(ctx.Base, ctx.Repo, form.Mode, form.Text, form.Context, form.FilePath, form.Wiki)
|
mode := util.Iif(form.Wiki, "wiki", form.Mode) //nolint:staticcheck
|
||||||
|
common.RenderMarkup(ctx.Base, ctx.Repo, mode, form.Text, form.Context, form.FilePath)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Markdown render markdown document to HTML
|
// Markdown render markdown document to HTML
|
||||||
|
@ -71,12 +73,8 @@ func Markdown(ctx *context.APIContext) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
mode := "markdown"
|
mode := util.Iif(form.Wiki, "wiki", form.Mode) //nolint:staticcheck
|
||||||
if form.Mode == "comment" || form.Mode == "gfm" {
|
common.RenderMarkup(ctx.Base, ctx.Repo, mode, form.Text, form.Context, "")
|
||||||
mode = form.Mode
|
|
||||||
}
|
|
||||||
|
|
||||||
common.RenderMarkup(ctx.Base, ctx.Repo, mode, form.Text, form.Context, "", form.Wiki)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// MarkdownRaw render raw markdown HTML
|
// MarkdownRaw render raw markdown HTML
|
||||||
|
|
|
@ -14,6 +14,7 @@ import (
|
||||||
"code.gitea.io/gitea/modules/markup"
|
"code.gitea.io/gitea/modules/markup"
|
||||||
"code.gitea.io/gitea/modules/setting"
|
"code.gitea.io/gitea/modules/setting"
|
||||||
api "code.gitea.io/gitea/modules/structs"
|
api "code.gitea.io/gitea/modules/structs"
|
||||||
|
"code.gitea.io/gitea/modules/test"
|
||||||
"code.gitea.io/gitea/modules/web"
|
"code.gitea.io/gitea/modules/web"
|
||||||
"code.gitea.io/gitea/services/contexttest"
|
"code.gitea.io/gitea/services/contexttest"
|
||||||
|
|
||||||
|
@ -24,6 +25,7 @@ const AppURL = "http://localhost:3000/"
|
||||||
|
|
||||||
func testRenderMarkup(t *testing.T, mode string, wiki bool, filePath, text, expectedBody string, expectedCode int) {
|
func testRenderMarkup(t *testing.T, mode string, wiki bool, filePath, text, expectedBody string, expectedCode int) {
|
||||||
setting.AppURL = AppURL
|
setting.AppURL = AppURL
|
||||||
|
defer test.MockVariableValue(&markup.RenderBehaviorForTesting.DisableInternalAttributes, true)()
|
||||||
context := "/gogits/gogs"
|
context := "/gogits/gogs"
|
||||||
if !wiki {
|
if !wiki {
|
||||||
context += path.Join("/src/branch/main", path.Dir(filePath))
|
context += path.Join("/src/branch/main", path.Dir(filePath))
|
||||||
|
@ -38,13 +40,13 @@ func testRenderMarkup(t *testing.T, mode string, wiki bool, filePath, text, expe
|
||||||
ctx, resp := contexttest.MockAPIContext(t, "POST /api/v1/markup")
|
ctx, resp := contexttest.MockAPIContext(t, "POST /api/v1/markup")
|
||||||
web.SetForm(ctx, &options)
|
web.SetForm(ctx, &options)
|
||||||
Markup(ctx)
|
Markup(ctx)
|
||||||
actual := strings.ReplaceAll(resp.Body.String(), ` data-markdown-generated-content=""`, "")
|
assert.Equal(t, expectedBody, resp.Body.String())
|
||||||
assert.Equal(t, expectedBody, actual)
|
|
||||||
assert.Equal(t, expectedCode, resp.Code)
|
assert.Equal(t, expectedCode, resp.Code)
|
||||||
resp.Body.Reset()
|
resp.Body.Reset()
|
||||||
}
|
}
|
||||||
|
|
||||||
func testRenderMarkdown(t *testing.T, mode string, wiki bool, text, responseBody string, responseCode int) {
|
func testRenderMarkdown(t *testing.T, mode string, wiki bool, text, responseBody string, responseCode int) {
|
||||||
|
defer test.MockVariableValue(&markup.RenderBehaviorForTesting.DisableInternalAttributes, true)()
|
||||||
setting.AppURL = AppURL
|
setting.AppURL = AppURL
|
||||||
context := "/gogits/gogs"
|
context := "/gogits/gogs"
|
||||||
if !wiki {
|
if !wiki {
|
||||||
|
@ -59,8 +61,7 @@ func testRenderMarkdown(t *testing.T, mode string, wiki bool, text, responseBody
|
||||||
ctx, resp := contexttest.MockAPIContext(t, "POST /api/v1/markdown")
|
ctx, resp := contexttest.MockAPIContext(t, "POST /api/v1/markdown")
|
||||||
web.SetForm(ctx, &options)
|
web.SetForm(ctx, &options)
|
||||||
Markdown(ctx)
|
Markdown(ctx)
|
||||||
actual := strings.ReplaceAll(resp.Body.String(), ` data-markdown-generated-content=""`, "")
|
assert.Equal(t, responseBody, resp.Body.String())
|
||||||
assert.Equal(t, responseBody, actual)
|
|
||||||
assert.Equal(t, responseCode, resp.Code)
|
assert.Equal(t, responseCode, resp.Code)
|
||||||
resp.Body.Reset()
|
resp.Body.Reset()
|
||||||
}
|
}
|
||||||
|
@ -158,8 +159,8 @@ Here are some links to the most important topics. You can find the full list of
|
||||||
<a href="http://localhost:3000/gogits/gogs/media/branch/main/path/image.png" target="_blank" rel="nofollow noopener"><img src="http://localhost:3000/gogits/gogs/media/branch/main/path/image.png" alt="Image"/></a></p>
|
<a href="http://localhost:3000/gogits/gogs/media/branch/main/path/image.png" target="_blank" rel="nofollow noopener"><img src="http://localhost:3000/gogits/gogs/media/branch/main/path/image.png" alt="Image"/></a></p>
|
||||||
`, http.StatusOK)
|
`, http.StatusOK)
|
||||||
|
|
||||||
testRenderMarkup(t, "file", true, "path/test.unknown", "## Test", "Unsupported render extension: .unknown\n", http.StatusUnprocessableEntity)
|
testRenderMarkup(t, "file", false, "path/test.unknown", "## Test", "unsupported file to render: \"path/test.unknown\"\n", http.StatusUnprocessableEntity)
|
||||||
testRenderMarkup(t, "unknown", true, "", "## Test", "Unknown mode: unknown\n", http.StatusUnprocessableEntity)
|
testRenderMarkup(t, "unknown", false, "", "## Test", "Unknown mode: unknown\n", http.StatusUnprocessableEntity)
|
||||||
}
|
}
|
||||||
|
|
||||||
var simpleCases = []string{
|
var simpleCases = []string{
|
||||||
|
|
|
@ -5,21 +5,22 @@
|
||||||
package common
|
package common
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
"path"
|
"path"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
repo_model "code.gitea.io/gitea/models/repo"
|
|
||||||
"code.gitea.io/gitea/modules/httplib"
|
"code.gitea.io/gitea/modules/httplib"
|
||||||
"code.gitea.io/gitea/modules/markup"
|
"code.gitea.io/gitea/modules/markup"
|
||||||
"code.gitea.io/gitea/modules/markup/markdown"
|
"code.gitea.io/gitea/modules/markup/markdown"
|
||||||
"code.gitea.io/gitea/modules/setting"
|
"code.gitea.io/gitea/modules/setting"
|
||||||
|
"code.gitea.io/gitea/modules/util"
|
||||||
"code.gitea.io/gitea/services/context"
|
"code.gitea.io/gitea/services/context"
|
||||||
)
|
)
|
||||||
|
|
||||||
// RenderMarkup renders markup text for the /markup and /markdown endpoints
|
// RenderMarkup renders markup text for the /markup and /markdown endpoints
|
||||||
func RenderMarkup(ctx *context.Base, repo *context.Repository, mode, text, urlPathContext, filePath string, wiki bool) {
|
func RenderMarkup(ctx *context.Base, repo *context.Repository, mode, text, urlPathContext, filePath string) {
|
||||||
// urlPathContext format is "/subpath/{user}/{repo}/src/{branch, commit, tag}/{identifier/path}/{file/dir}"
|
// urlPathContext format is "/subpath/{user}/{repo}/src/{branch, commit, tag}/{identifier/path}/{file/dir}"
|
||||||
// filePath is the path of the file to render if the end user is trying to preview a repo file (mode == "file")
|
// filePath is the path of the file to render if the end user is trying to preview a repo file (mode == "file")
|
||||||
// filePath will be used as RenderContext.RelativePath
|
// filePath will be used as RenderContext.RelativePath
|
||||||
|
@ -27,32 +28,33 @@ func RenderMarkup(ctx *context.Base, repo *context.Repository, mode, text, urlPa
|
||||||
// for example, when previewing file "/gitea/owner/repo/src/branch/features/feat-123/doc/CHANGE.md", then filePath is "doc/CHANGE.md"
|
// for example, when previewing file "/gitea/owner/repo/src/branch/features/feat-123/doc/CHANGE.md", then filePath is "doc/CHANGE.md"
|
||||||
// and the urlPathContext is "/gitea/owner/repo/src/branch/features/feat-123/doc"
|
// and the urlPathContext is "/gitea/owner/repo/src/branch/features/feat-123/doc"
|
||||||
|
|
||||||
var markupType, relativePath string
|
renderCtx := &markup.RenderContext{
|
||||||
|
Ctx: ctx,
|
||||||
links := markup.Links{AbsolutePrefix: true}
|
Links: markup.Links{AbsolutePrefix: true},
|
||||||
|
MarkupType: markdown.MarkupName,
|
||||||
|
}
|
||||||
if urlPathContext != "" {
|
if urlPathContext != "" {
|
||||||
links.Base = fmt.Sprintf("%s%s", httplib.GuessCurrentHostURL(ctx), urlPathContext)
|
renderCtx.Links.Base = fmt.Sprintf("%s%s", httplib.GuessCurrentHostURL(ctx), urlPathContext)
|
||||||
}
|
}
|
||||||
|
|
||||||
switch mode {
|
if mode == "" || mode == "markdown" {
|
||||||
case "markdown":
|
// raw markdown doesn't need any special handling
|
||||||
// Raw markdown
|
if err := markdown.RenderRaw(renderCtx, strings.NewReader(text), ctx.Resp); err != nil {
|
||||||
if err := markdown.RenderRaw(&markup.RenderContext{
|
|
||||||
Ctx: ctx,
|
|
||||||
Links: links,
|
|
||||||
}, strings.NewReader(text), ctx.Resp); err != nil {
|
|
||||||
ctx.Error(http.StatusInternalServerError, err.Error())
|
ctx.Error(http.StatusInternalServerError, err.Error())
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
|
}
|
||||||
|
switch mode {
|
||||||
|
case "gfm": // legacy mode, do nothing
|
||||||
case "comment":
|
case "comment":
|
||||||
// Issue & comment content
|
renderCtx.ContentMode = markup.RenderContentAsComment
|
||||||
markupType = markdown.MarkupName
|
case "wiki":
|
||||||
case "gfm":
|
renderCtx.ContentMode = markup.RenderContentAsWiki
|
||||||
// GitHub Flavored Markdown
|
|
||||||
markupType = markdown.MarkupName
|
|
||||||
case "file":
|
case "file":
|
||||||
markupType = "" // render the repo file content by its extension
|
// render the repo file content by its extension
|
||||||
relativePath = filePath
|
renderCtx.MarkupType = ""
|
||||||
|
renderCtx.RelativePath = filePath
|
||||||
|
renderCtx.InStandalonePage = true
|
||||||
default:
|
default:
|
||||||
ctx.Error(http.StatusUnprocessableEntity, fmt.Sprintf("Unknown mode: %s", mode))
|
ctx.Error(http.StatusUnprocessableEntity, fmt.Sprintf("Unknown mode: %s", mode))
|
||||||
return
|
return
|
||||||
|
@ -67,33 +69,19 @@ func RenderMarkup(ctx *context.Base, repo *context.Repository, mode, text, urlPa
|
||||||
refPath := strings.Join(fields[3:], "/") // it is "branch/features/feat-12/doc"
|
refPath := strings.Join(fields[3:], "/") // it is "branch/features/feat-12/doc"
|
||||||
refPath = strings.TrimSuffix(refPath, "/"+fileDir) // now we get the correct branch path: "branch/features/feat-12"
|
refPath = strings.TrimSuffix(refPath, "/"+fileDir) // now we get the correct branch path: "branch/features/feat-12"
|
||||||
|
|
||||||
links = markup.Links{AbsolutePrefix: true, Base: absoluteBasePrefix, BranchPath: refPath, TreePath: fileDir}
|
renderCtx.Links = markup.Links{AbsolutePrefix: true, Base: absoluteBasePrefix, BranchPath: refPath, TreePath: fileDir}
|
||||||
}
|
}
|
||||||
|
|
||||||
meta := map[string]string{}
|
|
||||||
var repoCtx *repo_model.Repository
|
|
||||||
if repo != nil && repo.Repository != nil {
|
if repo != nil && repo.Repository != nil {
|
||||||
repoCtx = repo.Repository
|
renderCtx.Repo = repo.Repository
|
||||||
if mode == "comment" {
|
if renderCtx.ContentMode == markup.RenderContentAsComment {
|
||||||
meta = repo.Repository.ComposeMetas(ctx)
|
renderCtx.Metas = repo.Repository.ComposeMetas(ctx)
|
||||||
} else {
|
} else {
|
||||||
meta = repo.Repository.ComposeDocumentMetas(ctx)
|
renderCtx.Metas = repo.Repository.ComposeDocumentMetas(ctx)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if mode != "comment" {
|
if err := markup.Render(renderCtx, strings.NewReader(text), ctx.Resp); err != nil {
|
||||||
meta["mode"] = "document"
|
if errors.Is(err, util.ErrInvalidArgument) {
|
||||||
}
|
|
||||||
|
|
||||||
if err := markup.Render(&markup.RenderContext{
|
|
||||||
Ctx: ctx,
|
|
||||||
Repo: repoCtx,
|
|
||||||
Links: links,
|
|
||||||
Metas: meta,
|
|
||||||
IsWiki: wiki,
|
|
||||||
Type: markupType,
|
|
||||||
RelativePath: relativePath,
|
|
||||||
}, strings.NewReader(text), ctx.Resp); err != nil {
|
|
||||||
if markup.IsErrUnsupportedRenderExtension(err) {
|
|
||||||
ctx.Error(http.StatusUnprocessableEntity, err.Error())
|
ctx.Error(http.StatusUnprocessableEntity, err.Error())
|
||||||
} else {
|
} else {
|
||||||
ctx.Error(http.StatusInternalServerError, err.Error())
|
ctx.Error(http.StatusInternalServerError, err.Error())
|
||||||
|
|
|
@ -56,7 +56,6 @@ func renderMarkdown(ctx *context.Context, act *activities_model.Action, content
|
||||||
Links: markup.Links{
|
Links: markup.Links{
|
||||||
Base: act.GetRepoLink(ctx),
|
Base: act.GetRepoLink(ctx),
|
||||||
},
|
},
|
||||||
Type: markdown.MarkupName,
|
|
||||||
Metas: map[string]string{
|
Metas: map[string]string{
|
||||||
"user": act.GetRepoUserName(ctx),
|
"user": act.GetRepoUserName(ctx),
|
||||||
"repo": act.GetRepoName(ctx),
|
"repo": act.GetRepoName(ctx),
|
||||||
|
|
|
@ -6,6 +6,7 @@ package misc
|
||||||
|
|
||||||
import (
|
import (
|
||||||
api "code.gitea.io/gitea/modules/structs"
|
api "code.gitea.io/gitea/modules/structs"
|
||||||
|
"code.gitea.io/gitea/modules/util"
|
||||||
"code.gitea.io/gitea/modules/web"
|
"code.gitea.io/gitea/modules/web"
|
||||||
"code.gitea.io/gitea/routers/common"
|
"code.gitea.io/gitea/routers/common"
|
||||||
"code.gitea.io/gitea/services/context"
|
"code.gitea.io/gitea/services/context"
|
||||||
|
@ -14,5 +15,6 @@ import (
|
||||||
// Markup render markup document to HTML
|
// Markup render markup document to HTML
|
||||||
func Markup(ctx *context.Context) {
|
func Markup(ctx *context.Context) {
|
||||||
form := web.GetForm(ctx).(*api.MarkupOption)
|
form := web.GetForm(ctx).(*api.MarkupOption)
|
||||||
common.RenderMarkup(ctx.Base, ctx.Repo, form.Mode, form.Text, form.Context, form.FilePath, form.Wiki)
|
mode := util.Iif(form.Wiki, "wiki", form.Mode) //nolint:staticcheck
|
||||||
|
common.RenderMarkup(ctx.Base, ctx.Repo, mode, form.Text, form.Context, form.FilePath)
|
||||||
}
|
}
|
||||||
|
|
|
@ -312,6 +312,7 @@ func renderReadmeFile(ctx *context.Context, subfolder string, readmeFile *git.Tr
|
||||||
|
|
||||||
ctx.Data["EscapeStatus"], ctx.Data["FileContent"], err = markupRender(ctx, &markup.RenderContext{
|
ctx.Data["EscapeStatus"], ctx.Data["FileContent"], err = markupRender(ctx, &markup.RenderContext{
|
||||||
Ctx: ctx,
|
Ctx: ctx,
|
||||||
|
MarkupType: markupType,
|
||||||
RelativePath: path.Join(ctx.Repo.TreePath, readmeFile.Name()), // ctx.Repo.TreePath is the directory not the Readme so we must append the Readme filename (and path).
|
RelativePath: path.Join(ctx.Repo.TreePath, readmeFile.Name()), // ctx.Repo.TreePath is the directory not the Readme so we must append the Readme filename (and path).
|
||||||
Links: markup.Links{
|
Links: markup.Links{
|
||||||
Base: ctx.Repo.RepoLink,
|
Base: ctx.Repo.RepoLink,
|
||||||
|
@ -502,28 +503,20 @@ func renderFile(ctx *context.Context, entry *git.TreeEntry) {
|
||||||
ctx.Data["ReadmeExist"] = readmeExist
|
ctx.Data["ReadmeExist"] = readmeExist
|
||||||
|
|
||||||
markupType := markup.DetectMarkupTypeByFileName(blob.Name())
|
markupType := markup.DetectMarkupTypeByFileName(blob.Name())
|
||||||
// If the markup is detected by custom markup renderer it should not be reset later on
|
|
||||||
// to not pass it down to the render context.
|
|
||||||
detected := false
|
|
||||||
if markupType == "" {
|
if markupType == "" {
|
||||||
detected = true
|
|
||||||
markupType = markup.DetectRendererType(blob.Name(), bytes.NewReader(buf))
|
markupType = markup.DetectRendererType(blob.Name(), bytes.NewReader(buf))
|
||||||
}
|
}
|
||||||
if markupType != "" {
|
if markupType != "" {
|
||||||
ctx.Data["HasSourceRenderedToggle"] = true
|
ctx.Data["HasSourceRenderedToggle"] = true
|
||||||
}
|
}
|
||||||
|
|
||||||
if markupType != "" && !shouldRenderSource {
|
if markupType != "" && !shouldRenderSource {
|
||||||
ctx.Data["IsMarkup"] = true
|
ctx.Data["IsMarkup"] = true
|
||||||
ctx.Data["MarkupType"] = markupType
|
ctx.Data["MarkupType"] = markupType
|
||||||
if !detected {
|
|
||||||
markupType = ""
|
|
||||||
}
|
|
||||||
metas := ctx.Repo.Repository.ComposeDocumentMetas(ctx)
|
metas := ctx.Repo.Repository.ComposeDocumentMetas(ctx)
|
||||||
metas["BranchNameSubURL"] = ctx.Repo.BranchNameSubURL()
|
metas["BranchNameSubURL"] = ctx.Repo.BranchNameSubURL()
|
||||||
ctx.Data["EscapeStatus"], ctx.Data["FileContent"], err = markupRender(ctx, &markup.RenderContext{
|
ctx.Data["EscapeStatus"], ctx.Data["FileContent"], err = markupRender(ctx, &markup.RenderContext{
|
||||||
Ctx: ctx,
|
Ctx: ctx,
|
||||||
Type: markupType,
|
MarkupType: markupType,
|
||||||
RelativePath: ctx.Repo.TreePath,
|
RelativePath: ctx.Repo.TreePath,
|
||||||
Links: markup.Links{
|
Links: markup.Links{
|
||||||
Base: ctx.Repo.RepoLink,
|
Base: ctx.Repo.RepoLink,
|
||||||
|
@ -615,6 +608,7 @@ func renderFile(ctx *context.Context, entry *git.TreeEntry) {
|
||||||
ctx.Data["MarkupType"] = markupType
|
ctx.Data["MarkupType"] = markupType
|
||||||
ctx.Data["EscapeStatus"], ctx.Data["FileContent"], err = markupRender(ctx, &markup.RenderContext{
|
ctx.Data["EscapeStatus"], ctx.Data["FileContent"], err = markupRender(ctx, &markup.RenderContext{
|
||||||
Ctx: ctx,
|
Ctx: ctx,
|
||||||
|
MarkupType: markupType,
|
||||||
RelativePath: ctx.Repo.TreePath,
|
RelativePath: ctx.Repo.TreePath,
|
||||||
Links: markup.Links{
|
Links: markup.Links{
|
||||||
Base: ctx.Repo.RepoLink,
|
Base: ctx.Repo.RepoLink,
|
||||||
|
|
|
@ -289,12 +289,12 @@ func renderViewPage(ctx *context.Context) (*git.Repository, *git.TreeEntry) {
|
||||||
}
|
}
|
||||||
|
|
||||||
rctx := &markup.RenderContext{
|
rctx := &markup.RenderContext{
|
||||||
Ctx: ctx,
|
Ctx: ctx,
|
||||||
Metas: ctx.Repo.Repository.ComposeDocumentMetas(ctx),
|
ContentMode: markup.RenderContentAsWiki,
|
||||||
|
Metas: ctx.Repo.Repository.ComposeDocumentMetas(ctx),
|
||||||
Links: markup.Links{
|
Links: markup.Links{
|
||||||
Base: ctx.Repo.RepoLink,
|
Base: ctx.Repo.RepoLink,
|
||||||
},
|
},
|
||||||
IsWiki: true,
|
|
||||||
}
|
}
|
||||||
buf := &strings.Builder{}
|
buf := &strings.Builder{}
|
||||||
|
|
||||||
|
|
|
@ -258,7 +258,6 @@ func prepareUserProfileTabData(ctx *context.Context, showPrivate bool, profileDb
|
||||||
Base: profileDbRepo.Link(),
|
Base: profileDbRepo.Link(),
|
||||||
BranchPath: path.Join("branch", util.PathEscapeSegments(profileDbRepo.DefaultBranch)),
|
BranchPath: path.Join("branch", util.PathEscapeSegments(profileDbRepo.DefaultBranch)),
|
||||||
},
|
},
|
||||||
Metas: map[string]string{"mode": "document"},
|
|
||||||
}, bytes); err != nil {
|
}, bytes); err != nil {
|
||||||
log.Error("failed to RenderString: %v", err)
|
log.Error("failed to RenderString: %v", err)
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -260,8 +260,7 @@ func HandleOrgAssignment(ctx *Context, args ...bool) {
|
||||||
ctx.Data["IsFollowing"] = ctx.Doer != nil && user_model.IsFollowing(ctx, ctx.Doer.ID, ctx.ContextUser.ID)
|
ctx.Data["IsFollowing"] = ctx.Doer != nil && user_model.IsFollowing(ctx, ctx.Doer.ID, ctx.ContextUser.ID)
|
||||||
if len(ctx.ContextUser.Description) != 0 {
|
if len(ctx.ContextUser.Description) != 0 {
|
||||||
content, err := markdown.RenderString(&markup.RenderContext{
|
content, err := markdown.RenderString(&markup.RenderContext{
|
||||||
Metas: map[string]string{"mode": "document"},
|
Ctx: ctx,
|
||||||
Ctx: ctx,
|
|
||||||
}, ctx.ContextUser.Description)
|
}, ctx.ContextUser.Description)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ctx.ServerError("RenderString", err)
|
ctx.ServerError("RenderString", err)
|
||||||
|
|
|
@ -8,6 +8,7 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
auth "code.gitea.io/gitea/models/auth"
|
auth "code.gitea.io/gitea/models/auth"
|
||||||
|
"code.gitea.io/gitea/models/db"
|
||||||
org_model "code.gitea.io/gitea/models/organization"
|
org_model "code.gitea.io/gitea/models/organization"
|
||||||
user_model "code.gitea.io/gitea/models/user"
|
user_model "code.gitea.io/gitea/models/user"
|
||||||
"code.gitea.io/gitea/modules/log"
|
"code.gitea.io/gitea/modules/log"
|
||||||
|
@ -192,7 +193,10 @@ func NewAccessTokenResponse(ctx context.Context, grant *auth.OAuth2Grant, server
|
||||||
// returns a list of "org" and "org:team" strings,
|
// returns a list of "org" and "org:team" strings,
|
||||||
// that the given user is a part of.
|
// that the given user is a part of.
|
||||||
func GetOAuthGroupsForUser(ctx context.Context, user *user_model.User) ([]string, error) {
|
func GetOAuthGroupsForUser(ctx context.Context, user *user_model.User) ([]string, error) {
|
||||||
orgs, err := org_model.GetUserOrgsList(ctx, user)
|
orgs, err := db.Find[org_model.Organization](ctx, org_model.FindOrgOptions{
|
||||||
|
UserID: user.ID,
|
||||||
|
IncludePrivate: true,
|
||||||
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("GetUserOrgList: %w", err)
|
return nil, fmt.Errorf("GetUserOrgList: %w", err)
|
||||||
}
|
}
|
||||||
|
|
|
@ -142,6 +142,7 @@ func CreateRelease(gitRepo *git.Repository, rel *repo_model.Release, attachmentU
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
rel.Title, _ = util.SplitStringAtByteN(rel.Title, 255)
|
||||||
rel.LowerTagName = strings.ToLower(rel.TagName)
|
rel.LowerTagName = strings.ToLower(rel.TagName)
|
||||||
if err = db.Insert(gitRepo.Ctx, rel); err != nil {
|
if err = db.Insert(gitRepo.Ctx, rel); err != nil {
|
||||||
return err
|
return err
|
||||||
|
|
|
@ -44,7 +44,7 @@ parts:
|
||||||
source: .
|
source: .
|
||||||
stage-packages: [ git, sqlite3, openssh-client ]
|
stage-packages: [ git, sqlite3, openssh-client ]
|
||||||
build-packages: [ git, libpam0g-dev, libsqlite3-dev, build-essential]
|
build-packages: [ git, libpam0g-dev, libsqlite3-dev, build-essential]
|
||||||
build-snaps: [ go/1.23/stable, node/20/stable ]
|
build-snaps: [ go/1.23/stable, node/22/stable ]
|
||||||
build-environment:
|
build-environment:
|
||||||
- LDFLAGS: ""
|
- LDFLAGS: ""
|
||||||
override-pull: |
|
override-pull: |
|
||||||
|
|
8
templates/swagger/v1_json.tmpl
generated
8
templates/swagger/v1_json.tmpl
generated
|
@ -22615,7 +22615,7 @@
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
"Mode": {
|
"Mode": {
|
||||||
"description": "Mode to render (comment, gfm, markdown)\n\nin: body",
|
"description": "Mode to render (markdown, comment, wiki, file)\n\nin: body",
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
"Text": {
|
"Text": {
|
||||||
|
@ -22623,7 +22623,7 @@
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
"Wiki": {
|
"Wiki": {
|
||||||
"description": "Is it a wiki page ?\n\nin: body",
|
"description": "Is it a wiki page? (use mode=wiki instead)\n\nDeprecated: true\nin: body",
|
||||||
"type": "boolean"
|
"type": "boolean"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -22642,7 +22642,7 @@
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
"Mode": {
|
"Mode": {
|
||||||
"description": "Mode to render (comment, gfm, markdown, file)\n\nin: body",
|
"description": "Mode to render (markdown, comment, wiki, file)\n\nin: body",
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
"Text": {
|
"Text": {
|
||||||
|
@ -22650,7 +22650,7 @@
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
"Wiki": {
|
"Wiki": {
|
||||||
"description": "Is it a wiki page ?\n\nin: body",
|
"description": "Is it a wiki page? (use mode=wiki instead)\n\nDeprecated: true\nin: body",
|
||||||
"type": "boolean"
|
"type": "boolean"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
|
@ -10,6 +10,8 @@ import (
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"code.gitea.io/gitea/modules/markup"
|
||||||
|
"code.gitea.io/gitea/modules/markup/external"
|
||||||
"code.gitea.io/gitea/modules/setting"
|
"code.gitea.io/gitea/modules/setting"
|
||||||
"code.gitea.io/gitea/tests"
|
"code.gitea.io/gitea/tests"
|
||||||
|
|
||||||
|
@ -23,10 +25,9 @@ func TestExternalMarkupRenderer(t *testing.T) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const repoURL = "user30/renderer"
|
req := NewRequest(t, "GET", "/user30/renderer/src/branch/master/README.html")
|
||||||
req := NewRequest(t, "GET", repoURL+"/src/branch/master/README.html")
|
|
||||||
resp := MakeRequest(t, req, http.StatusOK)
|
resp := MakeRequest(t, req, http.StatusOK)
|
||||||
assert.EqualValues(t, "text/html; charset=utf-8", resp.Header()["Content-Type"][0])
|
assert.EqualValues(t, "text/html; charset=utf-8", resp.Header().Get("Content-Type"))
|
||||||
|
|
||||||
bs, err := io.ReadAll(resp.Body)
|
bs, err := io.ReadAll(resp.Body)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
|
@ -36,4 +37,24 @@ func TestExternalMarkupRenderer(t *testing.T) {
|
||||||
data, err := div.Html()
|
data, err := div.Html()
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
assert.EqualValues(t, "<div>\n\ttest external renderer\n</div>", strings.TrimSpace(data))
|
assert.EqualValues(t, "<div>\n\ttest external renderer\n</div>", strings.TrimSpace(data))
|
||||||
|
|
||||||
|
r := markup.GetRendererByFileName("a.html").(*external.Renderer)
|
||||||
|
r.RenderContentMode = setting.RenderContentModeIframe
|
||||||
|
|
||||||
|
req = NewRequest(t, "GET", "/user30/renderer/src/branch/master/README.html")
|
||||||
|
resp = MakeRequest(t, req, http.StatusOK)
|
||||||
|
assert.EqualValues(t, "text/html; charset=utf-8", resp.Header().Get("Content-Type"))
|
||||||
|
bs, err = io.ReadAll(resp.Body)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
doc = NewHTMLParser(t, bytes.NewBuffer(bs))
|
||||||
|
iframe := doc.Find("iframe")
|
||||||
|
assert.EqualValues(t, "/user30/renderer/render/branch/master/README.html", iframe.AttrOr("src", ""))
|
||||||
|
|
||||||
|
req = NewRequest(t, "GET", "/user30/renderer/render/branch/master/README.html")
|
||||||
|
resp = MakeRequest(t, req, http.StatusOK)
|
||||||
|
assert.EqualValues(t, "text/html; charset=utf-8", resp.Header().Get("Content-Type"))
|
||||||
|
bs, err = io.ReadAll(resp.Body)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.EqualValues(t, "frame-src 'self'; sandbox allow-scripts", resp.Header().Get("Content-Security-Policy"))
|
||||||
|
assert.EqualValues(t, "<div>\n\ttest external renderer\n</div>\n", string(bs))
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import {createApp, nextTick} from 'vue';
|
import {createApp, nextTick} from 'vue';
|
||||||
import $ from 'jquery';
|
|
||||||
import {SvgIcon} from '../svg.ts';
|
import {SvgIcon} from '../svg.ts';
|
||||||
import {GET} from '../modules/fetch.ts';
|
import {GET} from '../modules/fetch.ts';
|
||||||
|
import {fomanticQuery} from '../modules/fomantic/base.ts';
|
||||||
|
|
||||||
const {appSubUrl, assetUrlPrefix, pageData} = window.config;
|
const {appSubUrl, assetUrlPrefix, pageData} = window.config;
|
||||||
|
|
||||||
|
@ -102,7 +102,7 @@ const sfc = {
|
||||||
mounted() {
|
mounted() {
|
||||||
const el = document.querySelector('#dashboard-repo-list');
|
const el = document.querySelector('#dashboard-repo-list');
|
||||||
this.changeReposFilter(this.reposFilter);
|
this.changeReposFilter(this.reposFilter);
|
||||||
$(el).find('.dropdown').dropdown();
|
fomanticQuery(el.querySelector('.ui.dropdown')).dropdown();
|
||||||
nextTick(() => {
|
nextTick(() => {
|
||||||
this.$refs.search.focus();
|
this.$refs.search.focus();
|
||||||
});
|
});
|
||||||
|
|
|
@ -21,7 +21,7 @@ import {
|
||||||
import {chartJsColors} from '../utils/color.ts';
|
import {chartJsColors} from '../utils/color.ts';
|
||||||
import {sleep} from '../utils.ts';
|
import {sleep} from '../utils.ts';
|
||||||
import 'chartjs-adapter-dayjs-4/dist/chartjs-adapter-dayjs-4.esm';
|
import 'chartjs-adapter-dayjs-4/dist/chartjs-adapter-dayjs-4.esm';
|
||||||
import $ from 'jquery';
|
import {fomanticQuery} from '../modules/fomantic/base.ts';
|
||||||
|
|
||||||
const customEventListener = {
|
const customEventListener = {
|
||||||
id: 'customEventListener',
|
id: 'customEventListener',
|
||||||
|
@ -77,7 +77,7 @@ export default {
|
||||||
mounted() {
|
mounted() {
|
||||||
this.fetchGraphData();
|
this.fetchGraphData();
|
||||||
|
|
||||||
$('#repo-contributors').dropdown({
|
fomanticQuery('#repo-contributors').dropdown({
|
||||||
onChange: (val) => {
|
onChange: (val) => {
|
||||||
this.xAxisMin = this.xAxisStart;
|
this.xAxisMin = this.xAxisStart;
|
||||||
this.xAxisMax = this.xAxisEnd;
|
this.xAxisMax = this.xAxisEnd;
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import $ from 'jquery';
|
|
||||||
import {getCurrentLocale} from '../utils.ts';
|
import {getCurrentLocale} from '../utils.ts';
|
||||||
|
import {fomanticQuery} from '../modules/fomantic/base.ts';
|
||||||
|
|
||||||
const {pageData} = window.config;
|
const {pageData} = window.config;
|
||||||
|
|
||||||
|
@ -71,6 +71,6 @@ export async function initCitationFileCopyContent() {
|
||||||
dropdownBtn.classList.remove('is-loading');
|
dropdownBtn.classList.remove('is-loading');
|
||||||
}
|
}
|
||||||
|
|
||||||
$('#cite-repo-modal').modal('show');
|
fomanticQuery('#cite-repo-modal').modal('show');
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,13 +1,12 @@
|
||||||
import $ from 'jquery';
|
import {applyAreYouSure, initAreYouSure} from '../vendor/jquery.are-you-sure.ts';
|
||||||
import {initAreYouSure} from '../vendor/jquery.are-you-sure.ts';
|
|
||||||
import {handleGlobalEnterQuickSubmit} from './comp/QuickSubmit.ts';
|
import {handleGlobalEnterQuickSubmit} from './comp/QuickSubmit.ts';
|
||||||
|
|
||||||
export function initGlobalFormDirtyLeaveConfirm() {
|
export function initGlobalFormDirtyLeaveConfirm() {
|
||||||
initAreYouSure(window.jQuery);
|
initAreYouSure(window.jQuery);
|
||||||
// Warn users that try to leave a page after entering data into a form.
|
// Warn users that try to leave a page after entering data into a form.
|
||||||
// Except on sign-in pages, and for forms marked as 'ignore-dirty'.
|
// Except on sign-in pages, and for forms marked as 'ignore-dirty'.
|
||||||
if (!$('.user.signin').length) {
|
if (!document.querySelector('.page-content.user.signin')) {
|
||||||
$('form:not(.ignore-dirty)').areYouSure();
|
applyAreYouSure('form:not(.ignore-dirty)');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -74,7 +74,6 @@ export class ComboMarkdownEditor {
|
||||||
previewUrl: string;
|
previewUrl: string;
|
||||||
previewContext: string;
|
previewContext: string;
|
||||||
previewMode: string;
|
previewMode: string;
|
||||||
previewWiki: boolean;
|
|
||||||
|
|
||||||
constructor(container, options = {}) {
|
constructor(container, options = {}) {
|
||||||
container._giteaComboMarkdownEditor = this;
|
container._giteaComboMarkdownEditor = this;
|
||||||
|
@ -213,13 +212,11 @@ export class ComboMarkdownEditor {
|
||||||
this.previewUrl = this.tabPreviewer.getAttribute('data-preview-url');
|
this.previewUrl = this.tabPreviewer.getAttribute('data-preview-url');
|
||||||
this.previewContext = this.tabPreviewer.getAttribute('data-preview-context');
|
this.previewContext = this.tabPreviewer.getAttribute('data-preview-context');
|
||||||
this.previewMode = this.options.previewMode ?? 'comment';
|
this.previewMode = this.options.previewMode ?? 'comment';
|
||||||
this.previewWiki = this.options.previewWiki ?? false;
|
|
||||||
this.tabPreviewer.addEventListener('click', async () => {
|
this.tabPreviewer.addEventListener('click', async () => {
|
||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
formData.append('mode', this.previewMode);
|
formData.append('mode', this.previewMode);
|
||||||
formData.append('context', this.previewContext);
|
formData.append('context', this.previewContext);
|
||||||
formData.append('text', this.value());
|
formData.append('text', this.value());
|
||||||
formData.append('wiki', String(this.previewWiki));
|
|
||||||
const response = await POST(this.previewUrl, {data: formData});
|
const response = await POST(this.previewUrl, {data: formData});
|
||||||
const data = await response.text();
|
const data = await response.text();
|
||||||
renderPreviewPanelContent($(panelPreviewer), data);
|
renderPreviewPanelContent($(panelPreviewer), data);
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import $ from 'jquery';
|
|
||||||
import {svg} from '../../svg.ts';
|
import {svg} from '../../svg.ts';
|
||||||
import {htmlEscape} from 'escape-goat';
|
import {htmlEscape} from 'escape-goat';
|
||||||
import {createElementFromHTML} from '../../utils/dom.ts';
|
import {createElementFromHTML} from '../../utils/dom.ts';
|
||||||
|
import {fomanticQuery} from '../../modules/fomantic/base.ts';
|
||||||
|
|
||||||
const {i18n} = window.config;
|
const {i18n} = window.config;
|
||||||
|
|
||||||
|
@ -17,7 +17,7 @@ export function confirmModal(content, {confirmButtonColor = 'primary'} = {}) {
|
||||||
</div>
|
</div>
|
||||||
`);
|
`);
|
||||||
document.body.append(modal);
|
document.body.append(modal);
|
||||||
const $modal = $(modal);
|
const $modal = fomanticQuery(modal);
|
||||||
$modal.modal({
|
$modal.modal({
|
||||||
onApprove() {
|
onApprove() {
|
||||||
resolve(true);
|
resolve(true);
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import $ from 'jquery';
|
|
||||||
import {POST} from '../../modules/fetch.ts';
|
import {POST} from '../../modules/fetch.ts';
|
||||||
|
import {fomanticQuery} from '../../modules/fomantic/base.ts';
|
||||||
|
|
||||||
export function initCompReactionSelector() {
|
export function initCompReactionSelector() {
|
||||||
for (const container of document.querySelectorAll('.issue-content, .diff-file-body')) {
|
for (const container of document.querySelectorAll('.issue-content, .diff-file-body')) {
|
||||||
|
@ -29,7 +29,7 @@ export function initCompReactionSelector() {
|
||||||
if (data.html) {
|
if (data.html) {
|
||||||
commentContainer.insertAdjacentHTML('beforeend', data.html);
|
commentContainer.insertAdjacentHTML('beforeend', data.html);
|
||||||
const bottomReactionsDropdowns = commentContainer.querySelectorAll('.bottom-reactions .dropdown.select-reaction');
|
const bottomReactionsDropdowns = commentContainer.querySelectorAll('.bottom-reactions .dropdown.select-reaction');
|
||||||
$(bottomReactionsDropdowns).dropdown(); // re-init the dropdown
|
fomanticQuery(bottomReactionsDropdowns).dropdown(); // re-init the dropdown
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import $ from 'jquery';
|
|
||||||
import {htmlEscape} from 'escape-goat';
|
import {htmlEscape} from 'escape-goat';
|
||||||
|
import {fomanticQuery} from '../../modules/fomantic/base.ts';
|
||||||
|
|
||||||
const {appSubUrl} = window.config;
|
const {appSubUrl} = window.config;
|
||||||
const looksLikeEmailAddressCheck = /^\S+@\S+$/;
|
const looksLikeEmailAddressCheck = /^\S+@\S+$/;
|
||||||
|
@ -10,7 +10,7 @@ export function initCompSearchUserBox() {
|
||||||
|
|
||||||
const allowEmailInput = searchUserBox.getAttribute('data-allow-email') === 'true';
|
const allowEmailInput = searchUserBox.getAttribute('data-allow-email') === 'true';
|
||||||
const allowEmailDescription = searchUserBox.getAttribute('data-allow-email-description') ?? undefined;
|
const allowEmailDescription = searchUserBox.getAttribute('data-allow-email-description') ?? undefined;
|
||||||
$(searchUserBox).search({
|
fomanticQuery(searchUserBox).search({
|
||||||
minCharacters: 2,
|
minCharacters: 2,
|
||||||
apiSettings: {
|
apiSettings: {
|
||||||
url: `${appSubUrl}/user/search_candidates?q={query}`,
|
url: `${appSubUrl}/user/search_candidates?q={query}`,
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import $ from 'jquery';
|
|
||||||
import {toggleElem} from '../utils/dom.ts';
|
import {toggleElem} from '../utils/dom.ts';
|
||||||
|
import {fomanticQuery} from '../modules/fomantic/base.ts';
|
||||||
|
|
||||||
export function initRepoBranchButton() {
|
export function initRepoBranchButton() {
|
||||||
initRepoCreateBranchButton();
|
initRepoCreateBranchButton();
|
||||||
|
@ -18,7 +18,7 @@ function initRepoCreateBranchButton() {
|
||||||
const fromSpanName = el.getAttribute('data-modal-from-span') || '#modal-create-branch-from-span';
|
const fromSpanName = el.getAttribute('data-modal-from-span') || '#modal-create-branch-from-span';
|
||||||
document.querySelector(fromSpanName).textContent = el.getAttribute('data-branch-from');
|
document.querySelector(fromSpanName).textContent = el.getAttribute('data-branch-from');
|
||||||
|
|
||||||
$(el.getAttribute('data-modal')).modal('show');
|
fomanticQuery(el.getAttribute('data-modal')).modal('show');
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import $ from 'jquery';
|
|
||||||
import {hideElem, showElem} from '../utils/dom.ts';
|
import {hideElem, showElem} from '../utils/dom.ts';
|
||||||
import {GET} from '../modules/fetch.ts';
|
import {GET} from '../modules/fetch.ts';
|
||||||
|
import {fomanticQuery} from '../modules/fomantic/base.ts';
|
||||||
|
|
||||||
export function initRepoGraphGit() {
|
export function initRepoGraphGit() {
|
||||||
const graphContainer = document.querySelector('#git-graph-container');
|
const graphContainer = document.querySelector('#git-graph-container');
|
||||||
|
@ -83,8 +83,8 @@ export function initRepoGraphGit() {
|
||||||
}
|
}
|
||||||
|
|
||||||
const flowSelectRefsDropdown = document.querySelector('#flow-select-refs-dropdown');
|
const flowSelectRefsDropdown = document.querySelector('#flow-select-refs-dropdown');
|
||||||
$(flowSelectRefsDropdown).dropdown('set selected', dropdownSelected);
|
fomanticQuery(flowSelectRefsDropdown).dropdown('set selected', dropdownSelected);
|
||||||
$(flowSelectRefsDropdown).dropdown({
|
fomanticQuery(flowSelectRefsDropdown).dropdown({
|
||||||
clearable: true,
|
clearable: true,
|
||||||
fullTextSeach: 'exact',
|
fullTextSeach: 'exact',
|
||||||
onRemove(toRemove) {
|
onRemove(toRemove) {
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
import $ from 'jquery';
|
|
||||||
import {stripTags} from '../utils.ts';
|
import {stripTags} from '../utils.ts';
|
||||||
import {hideElem, queryElemChildren, showElem} from '../utils/dom.ts';
|
import {hideElem, queryElemChildren, showElem} from '../utils/dom.ts';
|
||||||
import {POST} from '../modules/fetch.ts';
|
import {POST} from '../modules/fetch.ts';
|
||||||
import {showErrorToast} from '../modules/toast.ts';
|
import {showErrorToast} from '../modules/toast.ts';
|
||||||
|
import {fomanticQuery} from '../modules/fomantic/base.ts';
|
||||||
|
|
||||||
const {appSubUrl} = window.config;
|
const {appSubUrl} = window.config;
|
||||||
|
|
||||||
|
@ -73,7 +73,7 @@ export function initRepoTopicBar() {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
$(topicDropdown).dropdown({
|
fomanticQuery(topicDropdown).dropdown({
|
||||||
allowAdditions: true,
|
allowAdditions: true,
|
||||||
forceSelection: false,
|
forceSelection: false,
|
||||||
fullTextSearch: 'exact',
|
fullTextSearch: 'exact',
|
||||||
|
@ -136,7 +136,7 @@ export function initRepoTopicBar() {
|
||||||
onLabelCreate(value) {
|
onLabelCreate(value) {
|
||||||
value = value.toLowerCase().trim();
|
value = value.toLowerCase().trim();
|
||||||
this.attr('data-value', value).contents().first().replaceWith(value);
|
this.attr('data-value', value).contents().first().replaceWith(value);
|
||||||
return $(this);
|
return fomanticQuery(this);
|
||||||
},
|
},
|
||||||
onAdd(addedValue, _addedText, $addedChoice) {
|
onAdd(addedValue, _addedText, $addedChoice) {
|
||||||
addedValue = addedValue.toLowerCase().trim();
|
addedValue = addedValue.toLowerCase().trim();
|
||||||
|
|
|
@ -26,7 +26,6 @@ async function initRepoWikiFormEditor() {
|
||||||
formData.append('mode', editor.previewMode);
|
formData.append('mode', editor.previewMode);
|
||||||
formData.append('context', editor.previewContext);
|
formData.append('context', editor.previewContext);
|
||||||
formData.append('text', newContent);
|
formData.append('text', newContent);
|
||||||
formData.append('wiki', editor.previewWiki);
|
|
||||||
try {
|
try {
|
||||||
const response = await POST(editor.previewUrl, {data: formData});
|
const response = await POST(editor.previewUrl, {data: formData});
|
||||||
const data = await response.text();
|
const data = await response.text();
|
||||||
|
@ -51,8 +50,7 @@ async function initRepoWikiFormEditor() {
|
||||||
// And another benefit is that we only need to write the style once for both editors.
|
// And another benefit is that we only need to write the style once for both editors.
|
||||||
// TODO: Move height style to CSS after EasyMDE removal.
|
// TODO: Move height style to CSS after EasyMDE removal.
|
||||||
editorHeights: {minHeight: '300px', height: 'calc(100vh - 600px)'},
|
editorHeights: {minHeight: '300px', height: 'calc(100vh - 600px)'},
|
||||||
previewMode: 'gfm',
|
previewMode: 'wiki',
|
||||||
previewWiki: true,
|
|
||||||
easyMDEOptions: {
|
easyMDEOptions: {
|
||||||
previewRender: (_content, previewTarget) => previewTarget.innerHTML, // disable builtin preview render
|
previewRender: (_content, previewTarget) => previewTarget.innerHTML, // disable builtin preview render
|
||||||
toolbar: ['bold', 'italic', 'strikethrough', '|',
|
toolbar: ['bold', 'italic', 'strikethrough', '|',
|
||||||
|
|
4
web_src/js/vendor/jquery.are-you-sure.ts
vendored
4
web_src/js/vendor/jquery.are-you-sure.ts
vendored
|
@ -195,3 +195,7 @@ export function initAreYouSure($) {
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function applyAreYouSure(selector: string) {
|
||||||
|
$(selector).areYouSure();
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in a new issue