mirror of https://github.com/go-gitea/gitea.git
Merge remote-tracking branch 'origin/main' into xormigrate
This commit is contained in:
commit
a7b457b93b
|
@ -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
|
||||||
|
|
|
@ -154,12 +154,15 @@ jobs:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
services:
|
services:
|
||||||
mysql:
|
mysql:
|
||||||
image: mysql:8.0
|
# the bitnami mysql image has more options than the official one, it's easier to customize
|
||||||
|
image: bitnami/mysql:8.0
|
||||||
env:
|
env:
|
||||||
MYSQL_ALLOW_EMPTY_PASSWORD: true
|
ALLOW_EMPTY_PASSWORD: true
|
||||||
MYSQL_DATABASE: testgitea
|
MYSQL_DATABASE: testgitea
|
||||||
ports:
|
ports:
|
||||||
- "3306:3306"
|
- "3306:3306"
|
||||||
|
options: >-
|
||||||
|
--mount type=tmpfs,destination=/bitnami/mysql/data
|
||||||
elasticsearch:
|
elasticsearch:
|
||||||
image: elasticsearch:7.5.0
|
image: elasticsearch:7.5.0
|
||||||
env:
|
env:
|
||||||
|
@ -188,7 +191,8 @@ jobs:
|
||||||
- name: run migration tests
|
- name: run migration tests
|
||||||
run: make test-mysql-migration
|
run: make test-mysql-migration
|
||||||
- name: run tests
|
- name: run tests
|
||||||
run: make integration-test-coverage
|
# run: make integration-test-coverage (at the moment, no coverage is really handled)
|
||||||
|
run: make test-mysql
|
||||||
env:
|
env:
|
||||||
TAGS: bindata
|
TAGS: bindata
|
||||||
RACE_ENABLED: true
|
RACE_ENABLED: true
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -1912,7 +1912,7 @@ LEVEL = Info
|
||||||
;ENABLED = true
|
;ENABLED = true
|
||||||
;;
|
;;
|
||||||
;; Comma-separated list of allowed file extensions (`.zip`), mime types (`text/plain`) or wildcard type (`image/*`, `audio/*`, `video/*`). Empty value or `*/*` allows all types.
|
;; Comma-separated list of allowed file extensions (`.zip`), mime types (`text/plain`) or wildcard type (`image/*`, `audio/*`, `video/*`). Empty value or `*/*` allows all types.
|
||||||
;ALLOWED_TYPES = .csv,.docx,.fodg,.fodp,.fods,.fodt,.gif,.gz,.jpeg,.jpg,.log,.md,.mov,.mp4,.odf,.odg,.odp,.ods,.odt,.patch,.pdf,.png,.pptx,.svg,.tgz,.txt,.webm,.xls,.xlsx,.zip
|
;ALLOWED_TYPES = .avif,.cpuprofile,.csv,.dmp,.docx,.fodg,.fodp,.fods,.fodt,.gif,.gz,.jpeg,.jpg,.json,.jsonc,.log,.md,.mov,.mp4,.odf,.odg,.odp,.ods,.odt,.patch,.pdf,.png,.pptx,.svg,.tgz,.txt,.webm,.webp,.xls,.xlsx,.zip
|
||||||
;;
|
;;
|
||||||
;; Max size of each file. Defaults to 2048MB
|
;; Max size of each file. Defaults to 2048MB
|
||||||
;MAX_SIZE = 2048
|
;MAX_SIZE = 2048
|
||||||
|
|
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() {
|
||||||
|
|
|
@ -26,7 +26,7 @@
|
||||||
fork_id: 0
|
fork_id: 0
|
||||||
is_template: false
|
is_template: false
|
||||||
template_id: 0
|
template_id: 0
|
||||||
size: 8478
|
size: 0
|
||||||
is_fsck_enabled: true
|
is_fsck_enabled: true
|
||||||
close_issues_via_commit_in_any_branch: false
|
close_issues_via_commit_in_any_branch: false
|
||||||
|
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -8,7 +8,6 @@ import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"path"
|
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"runtime"
|
"runtime"
|
||||||
"testing"
|
"testing"
|
||||||
|
@ -16,7 +15,6 @@ import (
|
||||||
"code.gitea.io/gitea/models/unittest"
|
"code.gitea.io/gitea/models/unittest"
|
||||||
"code.gitea.io/gitea/modules/base"
|
"code.gitea.io/gitea/modules/base"
|
||||||
"code.gitea.io/gitea/modules/git"
|
"code.gitea.io/gitea/modules/git"
|
||||||
"code.gitea.io/gitea/modules/log"
|
|
||||||
"code.gitea.io/gitea/modules/setting"
|
"code.gitea.io/gitea/modules/setting"
|
||||||
"code.gitea.io/gitea/modules/testlogger"
|
"code.gitea.io/gitea/modules/testlogger"
|
||||||
|
|
||||||
|
@ -35,27 +33,7 @@ func PrepareTestEnv(t *testing.T, skip int, syncModels ...any) (*xorm.Engine, fu
|
||||||
ourSkip := 2
|
ourSkip := 2
|
||||||
ourSkip += skip
|
ourSkip += skip
|
||||||
deferFn := testlogger.PrintCurrentTest(t, ourSkip)
|
deferFn := testlogger.PrintCurrentTest(t, ourSkip)
|
||||||
assert.NoError(t, os.RemoveAll(setting.RepoRootPath))
|
assert.NoError(t, unittest.SyncDirs(filepath.Join(filepath.Dir(setting.AppPath), "tests/gitea-repositories-meta"), setting.RepoRootPath))
|
||||||
assert.NoError(t, unittest.CopyDir(path.Join(filepath.Dir(setting.AppPath), "tests/gitea-repositories-meta"), setting.RepoRootPath))
|
|
||||||
ownerDirs, err := os.ReadDir(setting.RepoRootPath)
|
|
||||||
if err != nil {
|
|
||||||
assert.NoError(t, err, "unable to read the new repo root: %v\n", err)
|
|
||||||
}
|
|
||||||
for _, ownerDir := range ownerDirs {
|
|
||||||
if !ownerDir.Type().IsDir() {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
repoDirs, err := os.ReadDir(filepath.Join(setting.RepoRootPath, ownerDir.Name()))
|
|
||||||
if err != nil {
|
|
||||||
assert.NoError(t, err, "unable to read the new repo root: %v\n", err)
|
|
||||||
}
|
|
||||||
for _, repoDir := range repoDirs {
|
|
||||||
_ = os.MkdirAll(filepath.Join(setting.RepoRootPath, ownerDir.Name(), repoDir.Name(), "objects", "pack"), 0o755)
|
|
||||||
_ = os.MkdirAll(filepath.Join(setting.RepoRootPath, ownerDir.Name(), repoDir.Name(), "objects", "info"), 0o755)
|
|
||||||
_ = os.MkdirAll(filepath.Join(setting.RepoRootPath, ownerDir.Name(), repoDir.Name(), "refs", "heads"), 0o755)
|
|
||||||
_ = os.MkdirAll(filepath.Join(setting.RepoRootPath, ownerDir.Name(), repoDir.Name(), "refs", "tag"), 0o755)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := deleteDB(); err != nil {
|
if err := deleteDB(); err != nil {
|
||||||
t.Errorf("unable to reset database: %v", err)
|
t.Errorf("unable to reset database: %v", err)
|
||||||
|
@ -112,39 +90,36 @@ func PrepareTestEnv(t *testing.T, skip int, syncModels ...any) (*xorm.Engine, fu
|
||||||
}
|
}
|
||||||
|
|
||||||
func MainTest(m *testing.M) {
|
func MainTest(m *testing.M) {
|
||||||
log.RegisterEventWriter("test", testlogger.NewTestLoggerWriter)
|
testlogger.Init()
|
||||||
|
|
||||||
giteaRoot := base.SetupGiteaRoot()
|
giteaRoot := base.SetupGiteaRoot()
|
||||||
if giteaRoot == "" {
|
if giteaRoot == "" {
|
||||||
fmt.Println("Environment variable $GITEA_ROOT not set")
|
testlogger.Fatalf("Environment variable $GITEA_ROOT not set\n")
|
||||||
os.Exit(1)
|
|
||||||
}
|
}
|
||||||
giteaBinary := "gitea"
|
giteaBinary := "gitea"
|
||||||
if runtime.GOOS == "windows" {
|
if runtime.GOOS == "windows" {
|
||||||
giteaBinary += ".exe"
|
giteaBinary += ".exe"
|
||||||
}
|
}
|
||||||
setting.AppPath = path.Join(giteaRoot, giteaBinary)
|
setting.AppPath = filepath.Join(giteaRoot, giteaBinary)
|
||||||
if _, err := os.Stat(setting.AppPath); err != nil {
|
if _, err := os.Stat(setting.AppPath); err != nil {
|
||||||
fmt.Printf("Could not find gitea binary at %s\n", setting.AppPath)
|
testlogger.Fatalf("Could not find gitea binary at %s\n", setting.AppPath)
|
||||||
os.Exit(1)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
giteaConf := os.Getenv("GITEA_CONF")
|
giteaConf := os.Getenv("GITEA_CONF")
|
||||||
if giteaConf == "" {
|
if giteaConf == "" {
|
||||||
giteaConf = path.Join(filepath.Dir(setting.AppPath), "tests/sqlite.ini")
|
giteaConf = filepath.Join(filepath.Dir(setting.AppPath), "tests/sqlite.ini")
|
||||||
fmt.Printf("Environment variable $GITEA_CONF not set - defaulting to %s\n", giteaConf)
|
fmt.Printf("Environment variable $GITEA_CONF not set - defaulting to %s\n", giteaConf)
|
||||||
}
|
}
|
||||||
|
|
||||||
if !path.IsAbs(giteaConf) {
|
if !filepath.IsAbs(giteaConf) {
|
||||||
setting.CustomConf = path.Join(giteaRoot, giteaConf)
|
setting.CustomConf = filepath.Join(giteaRoot, giteaConf)
|
||||||
} else {
|
} else {
|
||||||
setting.CustomConf = giteaConf
|
setting.CustomConf = giteaConf
|
||||||
}
|
}
|
||||||
|
|
||||||
tmpDataPath, err := os.MkdirTemp("", "data")
|
tmpDataPath, err := os.MkdirTemp("", "data")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Printf("Unable to create temporary data path %v\n", err)
|
testlogger.Fatalf("Unable to create temporary data path %v\n", err)
|
||||||
os.Exit(1)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
setting.CustomPath = filepath.Join(setting.AppWorkPath, "custom")
|
setting.CustomPath = filepath.Join(setting.AppWorkPath, "custom")
|
||||||
|
@ -152,8 +127,7 @@ func MainTest(m *testing.M) {
|
||||||
|
|
||||||
unittest.InitSettings()
|
unittest.InitSettings()
|
||||||
if err = git.InitFull(context.Background()); err != nil {
|
if err = git.InitFull(context.Background()); err != nil {
|
||||||
fmt.Printf("Unable to InitFull: %v\n", err)
|
testlogger.Fatalf("Unable to InitFull: %v\n", err)
|
||||||
os.Exit(1)
|
|
||||||
}
|
}
|
||||||
setting.LoadDBSetting()
|
setting.LoadDBSetting()
|
||||||
setting.InitLoggersForTest()
|
setting.InitLoggersForTest()
|
||||||
|
|
|
@ -350,6 +350,7 @@ func prepareMigrationTasks() []*xormigrate.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
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,304 +4,75 @@
|
||||||
package v1_23 //nolint
|
package v1_23 //nolint
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"code.gitea.io/gitea/modules/timeutil"
|
||||||
|
|
||||||
"code.gitea.io/gitea/modules/log"
|
|
||||||
"code.gitea.io/gitea/modules/setting"
|
|
||||||
|
|
||||||
"src.techknowlogick.com/xormigrate"
|
|
||||||
"xorm.io/xorm"
|
"xorm.io/xorm"
|
||||||
|
"xorm.io/xorm/schemas"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
type improveNotificationTableIndicesAction struct {
|
||||||
minDBVersion = 70 // Gitea 1.5.3
|
ID int64 `xorm:"pk autoincr"`
|
||||||
oldMigrationsCount = 230
|
UserID int64 `xorm:"NOT NULL"`
|
||||||
expectedVersion = minDBVersion + oldMigrationsCount
|
RepoID int64 `xorm:"NOT NULL"`
|
||||||
)
|
|
||||||
|
|
||||||
var oldMigrationNames = []string{
|
Status uint8 `xorm:"SMALLINT NOT NULL"`
|
||||||
"add issue_dependencies",
|
Source uint8 `xorm:"SMALLINT NOT NULL"`
|
||||||
"protect each scratch token",
|
|
||||||
"add review",
|
IssueID int64 `xorm:"NOT NULL"`
|
||||||
"add must_change_password column for users table",
|
CommitID string
|
||||||
"add approval whitelists to protected branches",
|
CommentID int64
|
||||||
"clear nonused data which not deleted when user was deleted",
|
|
||||||
"add pull request rebase with merge commit",
|
UpdatedBy int64 `xorm:"NOT NULL"`
|
||||||
"add theme to users",
|
|
||||||
"rename repo is_bare to repo is_empty",
|
CreatedUnix timeutil.TimeStamp `xorm:"created NOT NULL"`
|
||||||
"add can close issues via commit in any branch",
|
UpdatedUnix timeutil.TimeStamp `xorm:"updated NOT NULL"`
|
||||||
"add is locked to issues",
|
|
||||||
"update U2F counter type",
|
|
||||||
"hot fix for wrong release sha1 on release table",
|
|
||||||
"add uploader id for table attachment",
|
|
||||||
"add table to store original imported gpg keys",
|
|
||||||
"hash application token",
|
|
||||||
"add http method to webhook",
|
|
||||||
"add avatar field to repository",
|
|
||||||
"add commit status context field to commit_status",
|
|
||||||
"add original author/url migration info to issues, comments, and repo ",
|
|
||||||
"change length of some repository columns",
|
|
||||||
"add index on owner_id of repository and type, review_id of comment",
|
|
||||||
"remove orphaned repository index statuses",
|
|
||||||
"add email notification enabled preference to user",
|
|
||||||
"add enable_status_check, status_check_contexts to protected_branch",
|
|
||||||
"add table columns for cross referencing issues",
|
|
||||||
"delete orphaned attachments",
|
|
||||||
"add repo_admin_change_team_access to user",
|
|
||||||
"add original author name and id on migrated release",
|
|
||||||
"add task table and status column for repository table",
|
|
||||||
"update migration repositories' service type",
|
|
||||||
"change length of some external login users columns",
|
|
||||||
"update migration repositories' service type v2",
|
|
||||||
"Add WhitelistDeployKeys to protected branch",
|
|
||||||
"remove unnecessary columns from label",
|
|
||||||
"add includes_all_repositories to teams",
|
|
||||||
"add column `mode` to table watch",
|
|
||||||
"Add template options to repository",
|
|
||||||
"Add comment_id on table notification",
|
|
||||||
"add can_create_org_repo to team",
|
|
||||||
"change review content type to text",
|
|
||||||
"update branch protection for can push and whitelist enable",
|
|
||||||
"remove release attachments which repository deleted",
|
|
||||||
"new feature: change target branch of pull requests",
|
|
||||||
"Remove authentication credentials from stored URL",
|
|
||||||
"add user_id prefix to existing user avatar name",
|
|
||||||
"Extend TrackedTimes",
|
|
||||||
"Add block on rejected reviews branch protection",
|
|
||||||
"Add commit id and stale to reviews",
|
|
||||||
"Fix migrated repositories' git service type",
|
|
||||||
"Add owner_name on table repository",
|
|
||||||
"add is_restricted column for users table",
|
|
||||||
"Add Require Signed Commits to ProtectedBranch",
|
|
||||||
"Add original information for reactions",
|
|
||||||
"Add columns to user and repository",
|
|
||||||
"Add some columns on review for migration",
|
|
||||||
"Fix topic repository count",
|
|
||||||
"add repository code language statistics",
|
|
||||||
"fix merge base for pull requests",
|
|
||||||
"remove dependencies from deleted repositories",
|
|
||||||
"Expand webhooks for more granularity",
|
|
||||||
"Add IsSystemWebhook column to webhooks table",
|
|
||||||
"Add Branch Protection Protected Files Column",
|
|
||||||
"Add EmailHash Table",
|
|
||||||
"Refix merge base for merged pull requests",
|
|
||||||
"Add OrgID column to Labels table",
|
|
||||||
"Add CommitsAhead and CommitsBehind Column to PullRequest Table",
|
|
||||||
"Add Branch Protection Block Outdated Branch",
|
|
||||||
"Add ResolveDoerID to Comment table",
|
|
||||||
"prepend refs/heads/ to issue refs",
|
|
||||||
"Save detected language file size to database instead of percent",
|
|
||||||
"Add KeepActivityPrivate to User table",
|
|
||||||
"Ensure Repository.IsArchived is not null",
|
|
||||||
"recalculate Stars number for all user",
|
|
||||||
"update Matrix Webhook http method to 'PUT'",
|
|
||||||
"Increase Language field to 50 in LanguageStats",
|
|
||||||
"Add projects info to repository table",
|
|
||||||
"create review for 0 review id code comments",
|
|
||||||
"remove issue dependency comments who refer to non existing issues",
|
|
||||||
"Add Created and Updated to Milestone table",
|
|
||||||
"add primary key to repo_topic",
|
|
||||||
"set default password algorithm to Argon2",
|
|
||||||
"add TrustModel field to Repository",
|
|
||||||
"add Team review request support",
|
|
||||||
"add timestamps to Star, Label, Follow, Watch and Collaboration",
|
|
||||||
"add changed_protected_files column for pull_request table",
|
|
||||||
"fix publisher ID for tag releases",
|
|
||||||
"ensure repo topics are up-to-date",
|
|
||||||
"code comment replies should have the commitID of the review they are replying to",
|
|
||||||
"update reactions constraint",
|
|
||||||
"Add block on official review requests branch protection",
|
|
||||||
"Convert task type from int to string",
|
|
||||||
"Convert webhook task type from int to string",
|
|
||||||
"Convert topic name from 25 to 50",
|
|
||||||
"Add scope and nonce columns to oauth2_grant table",
|
|
||||||
"Convert hook task type from char(16) to varchar(16) and trim the column",
|
|
||||||
"Where Password is Valid with Empty String delete it",
|
|
||||||
"Add user redirect",
|
|
||||||
"Recreate user table to fix default values",
|
|
||||||
"Update DeleteBranch comments to set the old_ref to the commit_sha",
|
|
||||||
"Add Dismissed to Review table",
|
|
||||||
"Add Sorting to ProjectBoard table",
|
|
||||||
"Add sessions table for go-chi/session",
|
|
||||||
"Add time_id column to Comment",
|
|
||||||
"Create repo transfer table",
|
|
||||||
"Fix Postgres ID Sequences broken by recreate-table",
|
|
||||||
"Remove invalid labels from comments",
|
|
||||||
"Delete orphaned IssueLabels",
|
|
||||||
"Add LFS columns to Mirror",
|
|
||||||
"Convert avatar url to text",
|
|
||||||
"Delete credentials from past migrations",
|
|
||||||
"Always save primary email on email address table",
|
|
||||||
"Add issue resource index table",
|
|
||||||
"Create PushMirror table",
|
|
||||||
"Rename Task errors to message",
|
|
||||||
"Add new table repo_archiver",
|
|
||||||
"Create protected tag table",
|
|
||||||
"Drop unneeded webhook related columns",
|
|
||||||
"Add key is verified to gpg key",
|
|
||||||
"Unwrap ldap.Sources",
|
|
||||||
"Add agit flow pull request support",
|
|
||||||
"Alter issue/comment table TEXT fields to LONGTEXT",
|
|
||||||
"RecreateIssueResourceIndexTable to have a primary key instead of an unique index",
|
|
||||||
"Add repo id column for attachment table",
|
|
||||||
"Add Branch Protection Unprotected Files Column",
|
|
||||||
"Add table commit_status_index",
|
|
||||||
"Add Color to ProjectBoard table",
|
|
||||||
"Add renamed_branch table",
|
|
||||||
"Add issue content history table",
|
|
||||||
"No-op (remote version is using AppState now)",
|
|
||||||
"Add table app_state",
|
|
||||||
"Drop table remote_version (if exists)",
|
|
||||||
"Create key/value table for user settings",
|
|
||||||
"Add Sorting to ProjectIssue table",
|
|
||||||
"Add key is verified to ssh key",
|
|
||||||
"Migrate to higher varchar on user struct",
|
|
||||||
"Add authorize column to team_unit table",
|
|
||||||
"Add webauthn table and migrate u2f data to webauthn - NO-OPED",
|
|
||||||
"Use base32.HexEncoding instead of base64 encoding for cred ID as it is case insensitive - NO-OPED",
|
|
||||||
"Increase WebAuthentication CredentialID size to 410 - NO-OPED",
|
|
||||||
"v208 was completely broken - remigrate",
|
|
||||||
"Create ForeignReference table",
|
|
||||||
"Add package tables",
|
|
||||||
"Add allow edits from maintainers to PullRequest table",
|
|
||||||
"Add auto merge table",
|
|
||||||
"allow to view files in PRs",
|
|
||||||
"No-op (Improve Action table indices v1)",
|
|
||||||
"Alter hook_task table TEXT fields to LONGTEXT",
|
|
||||||
"Improve Action table indices v2",
|
|
||||||
"Add sync_on_commit column to push_mirror table",
|
|
||||||
"Add container repository property",
|
|
||||||
"Store WebAuthentication CredentialID as bytes and increase size to at least 1024",
|
|
||||||
"Drop old CredentialID column",
|
|
||||||
"Rename CredentialIDBytes column to CredentialID",
|
|
||||||
"Add badges to users",
|
|
||||||
"Alter gpg_key/public_key content TEXT fields to MEDIUMTEXT",
|
|
||||||
"Conan and generic packages do not need to be semantically versioned",
|
|
||||||
"Create key/value table for system settings",
|
|
||||||
"Add TeamInvite table",
|
|
||||||
"Update counts of all open milestones",
|
|
||||||
"Add ConfidentialClient column (default true) to OAuth2Application table",
|
|
||||||
"Add index for hook_task",
|
|
||||||
"Alter package_version.metadata_json to LONGTEXT",
|
|
||||||
"Add header_authorization_encrypted column to webhook table",
|
|
||||||
"Add package cleanup rule table",
|
|
||||||
"Add index for access_token",
|
|
||||||
"Create secrets table",
|
|
||||||
"Drop ForeignReference table",
|
|
||||||
"Add updated unix to LFSMetaObject",
|
|
||||||
"Add scope for access_token",
|
|
||||||
"Add actions tables",
|
|
||||||
"Add card_type column to project table",
|
|
||||||
"Alter gpg_key_import content TEXT field to MEDIUMTEXT",
|
|
||||||
"Add exclusive label",
|
|
||||||
"Add NeedApproval to actions tables",
|
|
||||||
"Rename Webhook org_id to owner_id",
|
|
||||||
"Add missed column owner_id for project table",
|
|
||||||
"Fix incorrect project type",
|
|
||||||
"Add version column to action_runner table",
|
|
||||||
"Improve Action table indices v3",
|
|
||||||
"Change Container Metadata",
|
|
||||||
"Fix incorrect owner team unit access mode",
|
|
||||||
"Fix incorrect admin team unit access mode",
|
|
||||||
"Fix ExternalTracker and ExternalWiki accessMode in owner and admin team",
|
|
||||||
"Add ActionTaskOutput table",
|
|
||||||
"Add ArchivedUnix Column",
|
|
||||||
"Add is_internal column to package",
|
|
||||||
"Add Actions Artifact table",
|
|
||||||
"Add PinOrder Column",
|
|
||||||
"Convert scoped access tokens",
|
|
||||||
"Drop custom_labels column of action_runner table",
|
|
||||||
"Add variable table",
|
|
||||||
"Add TriggerEvent to action_run table",
|
|
||||||
"Add git_size and lfs_size columns to repository table",
|
|
||||||
"Add branch table",
|
|
||||||
"Alter Actions Artifact table",
|
|
||||||
"Reduce commit status",
|
|
||||||
"Add action_tasks_version table",
|
|
||||||
"Update Action Ref",
|
|
||||||
"Drop deleted branch table",
|
|
||||||
"Fix PackageProperty typo",
|
|
||||||
"Allow archiving labels",
|
|
||||||
"Add Version to ActionRun table",
|
|
||||||
"Add Action Schedule Table",
|
|
||||||
"Add Actions artifacts expiration date",
|
|
||||||
"Add ScheduleID for ActionRun",
|
|
||||||
"Add RemoteAddress to mirrors",
|
|
||||||
"Add Index to issue_user.issue_id",
|
|
||||||
"Add Index to comment.dependent_issue_id",
|
|
||||||
"Add Index to action.user_id",
|
|
||||||
"Rename user themes",
|
|
||||||
"Add auth_token table",
|
|
||||||
"Add Index to pull_auto_merge.doer_id",
|
|
||||||
"Add combined Index to issue_user.uid and issue_id",
|
|
||||||
"Add ignore stale approval column on branch table",
|
|
||||||
"Add PreviousDuration to ActionRun",
|
|
||||||
"Add support for SHA256 git repositories",
|
|
||||||
"Use Slug instead of ID for Badges",
|
|
||||||
"Add user_blocking table",
|
|
||||||
"Add default_wiki_branch to repository table",
|
|
||||||
"Add PayloadVersion to HookTask",
|
|
||||||
"Add Index to attachment.comment_id",
|
|
||||||
"Ensure every project has exactly one default column - No Op",
|
|
||||||
"Ensure every project has exactly one default column",
|
|
||||||
"Add unique index for project issue table",
|
|
||||||
"Add commit status summary table",
|
|
||||||
"Add missing field of commit status summary table",
|
|
||||||
"Add everyone_access_mode for repo_unit",
|
|
||||||
"Drop wrongly created table o_auth2_application",
|
|
||||||
"Add content version to issue and comment table",
|
|
||||||
"Add force-push branch protection support",
|
|
||||||
"Add skip_secondary_authorization option to oauth2 application table",
|
|
||||||
"Add metadata column for comment table",
|
|
||||||
"Add index for release sha1",
|
|
||||||
"Add Repository Licenses",
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Version describes the version table. Should have only one row with id==1
|
// TableName sets the name of this table
|
||||||
type Version struct {
|
func (*improveNotificationTableIndicesAction) TableName() string {
|
||||||
ID int64 `xorm:"pk autoincr"`
|
return "notification"
|
||||||
Version int64
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func MigrateToXormigrate(x *xorm.Engine) error {
|
// TableIndices implements xorm's TableIndices interface
|
||||||
if err := x.Sync(new(Version)); err != nil {
|
func (*improveNotificationTableIndicesAction) TableIndices() []*schemas.Index {
|
||||||
return fmt.Errorf("sync: %w", err)
|
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)
|
||||||
|
|
||||||
currentVersion := &Version{ID: 1}
|
// Add the individual indices that were previously defined in struct tags
|
||||||
has, err := x.Get(currentVersion)
|
userIDIndex := schemas.NewIndex("idx_notification_user_id", schemas.IndexType)
|
||||||
if err != nil {
|
userIDIndex.AddColumn("user_id")
|
||||||
return fmt.Errorf("get: %w", err)
|
indices = append(indices, userIDIndex)
|
||||||
} else if !has {
|
|
||||||
// This should not happen
|
|
||||||
return fmt.Errorf("could not get version")
|
|
||||||
}
|
|
||||||
|
|
||||||
v := currentVersion.Version
|
repoIDIndex := schemas.NewIndex("idx_notification_repo_id", schemas.IndexType)
|
||||||
if minDBVersion > v {
|
repoIDIndex.AddColumn("repo_id")
|
||||||
log.Fatal(`Gitea no longer supports auto-migration from your previously installed version.
|
indices = append(indices, repoIDIndex)
|
||||||
Please try upgrading to i.e. v1.6.4 first, then upgrade to this version.`)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Downgrading Gitea's database version not supported
|
statusIndex := schemas.NewIndex("idx_notification_status", schemas.IndexType)
|
||||||
if int(v-minDBVersion) > oldMigrationsCount {
|
statusIndex.AddColumn("status")
|
||||||
msg := fmt.Sprintf("Your database (migration version: %d) is for a newer Gitea, you can not use the newer database for this old Gitea release (%d).", v, expectedVersion)
|
indices = append(indices, statusIndex)
|
||||||
msg += "\nGitea will exit to keep your database safe and unchanged. Please use the correct Gitea release, do not change the migration version manually (incorrect manual operation may lose data)."
|
|
||||||
if !setting.IsProd {
|
|
||||||
msg += fmt.Sprintf("\nIf you are in development and really know what you're doing, you can force changing the migration version by executing: UPDATE version SET version=%d WHERE id=1;", expectedVersion)
|
|
||||||
}
|
|
||||||
log.Fatal("Migration Error: %s", msg)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// add migrations that already have been run
|
sourceIndex := schemas.NewIndex("idx_notification_source", schemas.IndexType)
|
||||||
for i := v; i < 307; i++ {
|
sourceIndex.AddColumn("source")
|
||||||
if _, err := x.Insert(&xormigrate.Migration{ID: fmt.Sprint(i)}); err != nil {
|
indices = append(indices, sourceIndex)
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Remove old version table
|
issueIDIndex := schemas.NewIndex("idx_notification_issue_id", schemas.IndexType)
|
||||||
return x.DropTables(new(Version))
|
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{})
|
||||||
|
>>>>>>> origin/main
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,311 @@
|
||||||
|
// Copyright 2024 The Gitea Authors. All rights reserved.
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
package v1_23 //nolint
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"code.gitea.io/gitea/modules/log"
|
||||||
|
"code.gitea.io/gitea/modules/setting"
|
||||||
|
|
||||||
|
"src.techknowlogick.com/xormigrate"
|
||||||
|
"xorm.io/xorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
minDBVersion = 70 // Gitea 1.5.3
|
||||||
|
oldMigrationsCount = 230
|
||||||
|
expectedVersion = minDBVersion + oldMigrationsCount
|
||||||
|
)
|
||||||
|
|
||||||
|
var oldMigrationNames = []string{
|
||||||
|
"add issue_dependencies",
|
||||||
|
"protect each scratch token",
|
||||||
|
"add review",
|
||||||
|
"add must_change_password column for users table",
|
||||||
|
"add approval whitelists to protected branches",
|
||||||
|
"clear nonused data which not deleted when user was deleted",
|
||||||
|
"add pull request rebase with merge commit",
|
||||||
|
"add theme to users",
|
||||||
|
"rename repo is_bare to repo is_empty",
|
||||||
|
"add can close issues via commit in any branch",
|
||||||
|
"add is locked to issues",
|
||||||
|
"update U2F counter type",
|
||||||
|
"hot fix for wrong release sha1 on release table",
|
||||||
|
"add uploader id for table attachment",
|
||||||
|
"add table to store original imported gpg keys",
|
||||||
|
"hash application token",
|
||||||
|
"add http method to webhook",
|
||||||
|
"add avatar field to repository",
|
||||||
|
"add commit status context field to commit_status",
|
||||||
|
"add original author/url migration info to issues, comments, and repo ",
|
||||||
|
"change length of some repository columns",
|
||||||
|
"add index on owner_id of repository and type, review_id of comment",
|
||||||
|
"remove orphaned repository index statuses",
|
||||||
|
"add email notification enabled preference to user",
|
||||||
|
"add enable_status_check, status_check_contexts to protected_branch",
|
||||||
|
"add table columns for cross referencing issues",
|
||||||
|
"delete orphaned attachments",
|
||||||
|
"add repo_admin_change_team_access to user",
|
||||||
|
"add original author name and id on migrated release",
|
||||||
|
"add task table and status column for repository table",
|
||||||
|
"update migration repositories' service type",
|
||||||
|
"change length of some external login users columns",
|
||||||
|
"update migration repositories' service type v2",
|
||||||
|
"Add WhitelistDeployKeys to protected branch",
|
||||||
|
"remove unnecessary columns from label",
|
||||||
|
"add includes_all_repositories to teams",
|
||||||
|
"add column `mode` to table watch",
|
||||||
|
"Add template options to repository",
|
||||||
|
"Add comment_id on table notification",
|
||||||
|
"add can_create_org_repo to team",
|
||||||
|
"change review content type to text",
|
||||||
|
"update branch protection for can push and whitelist enable",
|
||||||
|
"remove release attachments which repository deleted",
|
||||||
|
"new feature: change target branch of pull requests",
|
||||||
|
"Remove authentication credentials from stored URL",
|
||||||
|
"add user_id prefix to existing user avatar name",
|
||||||
|
"Extend TrackedTimes",
|
||||||
|
"Add block on rejected reviews branch protection",
|
||||||
|
"Add commit id and stale to reviews",
|
||||||
|
"Fix migrated repositories' git service type",
|
||||||
|
"Add owner_name on table repository",
|
||||||
|
"add is_restricted column for users table",
|
||||||
|
"Add Require Signed Commits to ProtectedBranch",
|
||||||
|
"Add original information for reactions",
|
||||||
|
"Add columns to user and repository",
|
||||||
|
"Add some columns on review for migration",
|
||||||
|
"Fix topic repository count",
|
||||||
|
"add repository code language statistics",
|
||||||
|
"fix merge base for pull requests",
|
||||||
|
"remove dependencies from deleted repositories",
|
||||||
|
"Expand webhooks for more granularity",
|
||||||
|
"Add IsSystemWebhook column to webhooks table",
|
||||||
|
"Add Branch Protection Protected Files Column",
|
||||||
|
"Add EmailHash Table",
|
||||||
|
"Refix merge base for merged pull requests",
|
||||||
|
"Add OrgID column to Labels table",
|
||||||
|
"Add CommitsAhead and CommitsBehind Column to PullRequest Table",
|
||||||
|
"Add Branch Protection Block Outdated Branch",
|
||||||
|
"Add ResolveDoerID to Comment table",
|
||||||
|
"prepend refs/heads/ to issue refs",
|
||||||
|
"Save detected language file size to database instead of percent",
|
||||||
|
"Add KeepActivityPrivate to User table",
|
||||||
|
"Ensure Repository.IsArchived is not null",
|
||||||
|
"recalculate Stars number for all user",
|
||||||
|
"update Matrix Webhook http method to 'PUT'",
|
||||||
|
"Increase Language field to 50 in LanguageStats",
|
||||||
|
"Add projects info to repository table",
|
||||||
|
"create review for 0 review id code comments",
|
||||||
|
"remove issue dependency comments who refer to non existing issues",
|
||||||
|
"Add Created and Updated to Milestone table",
|
||||||
|
"add primary key to repo_topic",
|
||||||
|
"set default password algorithm to Argon2",
|
||||||
|
"add TrustModel field to Repository",
|
||||||
|
"add Team review request support",
|
||||||
|
"add timestamps to Star, Label, Follow, Watch and Collaboration",
|
||||||
|
"add changed_protected_files column for pull_request table",
|
||||||
|
"fix publisher ID for tag releases",
|
||||||
|
"ensure repo topics are up-to-date",
|
||||||
|
"code comment replies should have the commitID of the review they are replying to",
|
||||||
|
"update reactions constraint",
|
||||||
|
"Add block on official review requests branch protection",
|
||||||
|
"Convert task type from int to string",
|
||||||
|
"Convert webhook task type from int to string",
|
||||||
|
"Convert topic name from 25 to 50",
|
||||||
|
"Add scope and nonce columns to oauth2_grant table",
|
||||||
|
"Convert hook task type from char(16) to varchar(16) and trim the column",
|
||||||
|
"Where Password is Valid with Empty String delete it",
|
||||||
|
"Add user redirect",
|
||||||
|
"Recreate user table to fix default values",
|
||||||
|
"Update DeleteBranch comments to set the old_ref to the commit_sha",
|
||||||
|
"Add Dismissed to Review table",
|
||||||
|
"Add Sorting to ProjectBoard table",
|
||||||
|
"Add sessions table for go-chi/session",
|
||||||
|
"Add time_id column to Comment",
|
||||||
|
"Create repo transfer table",
|
||||||
|
"Fix Postgres ID Sequences broken by recreate-table",
|
||||||
|
"Remove invalid labels from comments",
|
||||||
|
"Delete orphaned IssueLabels",
|
||||||
|
"Add LFS columns to Mirror",
|
||||||
|
"Convert avatar url to text",
|
||||||
|
"Delete credentials from past migrations",
|
||||||
|
"Always save primary email on email address table",
|
||||||
|
"Add issue resource index table",
|
||||||
|
"Create PushMirror table",
|
||||||
|
"Rename Task errors to message",
|
||||||
|
"Add new table repo_archiver",
|
||||||
|
"Create protected tag table",
|
||||||
|
"Drop unneeded webhook related columns",
|
||||||
|
"Add key is verified to gpg key",
|
||||||
|
"Unwrap ldap.Sources",
|
||||||
|
"Add agit flow pull request support",
|
||||||
|
"Alter issue/comment table TEXT fields to LONGTEXT",
|
||||||
|
"RecreateIssueResourceIndexTable to have a primary key instead of an unique index",
|
||||||
|
"Add repo id column for attachment table",
|
||||||
|
"Add Branch Protection Unprotected Files Column",
|
||||||
|
"Add table commit_status_index",
|
||||||
|
"Add Color to ProjectBoard table",
|
||||||
|
"Add renamed_branch table",
|
||||||
|
"Add issue content history table",
|
||||||
|
"No-op (remote version is using AppState now)",
|
||||||
|
"Add table app_state",
|
||||||
|
"Drop table remote_version (if exists)",
|
||||||
|
"Create key/value table for user settings",
|
||||||
|
"Add Sorting to ProjectIssue table",
|
||||||
|
"Add key is verified to ssh key",
|
||||||
|
"Migrate to higher varchar on user struct",
|
||||||
|
"Add authorize column to team_unit table",
|
||||||
|
"Add webauthn table and migrate u2f data to webauthn - NO-OPED",
|
||||||
|
"Use base32.HexEncoding instead of base64 encoding for cred ID as it is case insensitive - NO-OPED",
|
||||||
|
"Increase WebAuthentication CredentialID size to 410 - NO-OPED",
|
||||||
|
"v208 was completely broken - remigrate",
|
||||||
|
"Create ForeignReference table",
|
||||||
|
"Add package tables",
|
||||||
|
"Add allow edits from maintainers to PullRequest table",
|
||||||
|
"Add auto merge table",
|
||||||
|
"allow to view files in PRs",
|
||||||
|
"No-op (Improve Action table indices v1)",
|
||||||
|
"Alter hook_task table TEXT fields to LONGTEXT",
|
||||||
|
"Improve Action table indices v2",
|
||||||
|
"Add sync_on_commit column to push_mirror table",
|
||||||
|
"Add container repository property",
|
||||||
|
"Store WebAuthentication CredentialID as bytes and increase size to at least 1024",
|
||||||
|
"Drop old CredentialID column",
|
||||||
|
"Rename CredentialIDBytes column to CredentialID",
|
||||||
|
"Add badges to users",
|
||||||
|
"Alter gpg_key/public_key content TEXT fields to MEDIUMTEXT",
|
||||||
|
"Conan and generic packages do not need to be semantically versioned",
|
||||||
|
"Create key/value table for system settings",
|
||||||
|
"Add TeamInvite table",
|
||||||
|
"Update counts of all open milestones",
|
||||||
|
"Add ConfidentialClient column (default true) to OAuth2Application table",
|
||||||
|
"Add index for hook_task",
|
||||||
|
"Alter package_version.metadata_json to LONGTEXT",
|
||||||
|
"Add header_authorization_encrypted column to webhook table",
|
||||||
|
"Add package cleanup rule table",
|
||||||
|
"Add index for access_token",
|
||||||
|
"Create secrets table",
|
||||||
|
"Drop ForeignReference table",
|
||||||
|
"Add updated unix to LFSMetaObject",
|
||||||
|
"Add scope for access_token",
|
||||||
|
"Add actions tables",
|
||||||
|
"Add card_type column to project table",
|
||||||
|
"Alter gpg_key_import content TEXT field to MEDIUMTEXT",
|
||||||
|
"Add exclusive label",
|
||||||
|
"Add NeedApproval to actions tables",
|
||||||
|
"Rename Webhook org_id to owner_id",
|
||||||
|
"Add missed column owner_id for project table",
|
||||||
|
"Fix incorrect project type",
|
||||||
|
"Add version column to action_runner table",
|
||||||
|
"Improve Action table indices v3",
|
||||||
|
"Change Container Metadata",
|
||||||
|
"Fix incorrect owner team unit access mode",
|
||||||
|
"Fix incorrect admin team unit access mode",
|
||||||
|
"Fix ExternalTracker and ExternalWiki accessMode in owner and admin team",
|
||||||
|
"Add ActionTaskOutput table",
|
||||||
|
"Add ArchivedUnix Column",
|
||||||
|
"Add is_internal column to package",
|
||||||
|
"Add Actions Artifact table",
|
||||||
|
"Add PinOrder Column",
|
||||||
|
"Convert scoped access tokens",
|
||||||
|
"Drop custom_labels column of action_runner table",
|
||||||
|
"Add variable table",
|
||||||
|
"Add TriggerEvent to action_run table",
|
||||||
|
"Add git_size and lfs_size columns to repository table",
|
||||||
|
"Add branch table",
|
||||||
|
"Alter Actions Artifact table",
|
||||||
|
"Reduce commit status",
|
||||||
|
"Add action_tasks_version table",
|
||||||
|
"Update Action Ref",
|
||||||
|
"Drop deleted branch table",
|
||||||
|
"Fix PackageProperty typo",
|
||||||
|
"Allow archiving labels",
|
||||||
|
"Add Version to ActionRun table",
|
||||||
|
"Add Action Schedule Table",
|
||||||
|
"Add Actions artifacts expiration date",
|
||||||
|
"Add ScheduleID for ActionRun",
|
||||||
|
"Add RemoteAddress to mirrors",
|
||||||
|
"Add Index to issue_user.issue_id",
|
||||||
|
"Add Index to comment.dependent_issue_id",
|
||||||
|
"Add Index to action.user_id",
|
||||||
|
"Rename user themes",
|
||||||
|
"Add auth_token table",
|
||||||
|
"Add Index to pull_auto_merge.doer_id",
|
||||||
|
"Add combined Index to issue_user.uid and issue_id",
|
||||||
|
"Add ignore stale approval column on branch table",
|
||||||
|
"Add PreviousDuration to ActionRun",
|
||||||
|
"Add support for SHA256 git repositories",
|
||||||
|
"Use Slug instead of ID for Badges",
|
||||||
|
"Add user_blocking table",
|
||||||
|
"Add default_wiki_branch to repository table",
|
||||||
|
"Add PayloadVersion to HookTask",
|
||||||
|
"Add Index to attachment.comment_id",
|
||||||
|
"Ensure every project has exactly one default column - No Op",
|
||||||
|
"Ensure every project has exactly one default column",
|
||||||
|
"Add unique index for project issue table",
|
||||||
|
"Add commit status summary table",
|
||||||
|
"Add missing field of commit status summary table",
|
||||||
|
"Add everyone_access_mode for repo_unit",
|
||||||
|
"Drop wrongly created table o_auth2_application",
|
||||||
|
"Add content version to issue and comment table",
|
||||||
|
"Add force-push branch protection support",
|
||||||
|
"Add skip_secondary_authorization option to oauth2 application table",
|
||||||
|
"Add metadata column for comment table",
|
||||||
|
"Add index for release sha1",
|
||||||
|
"Add Repository Licenses",
|
||||||
|
"Improve Notification table indices",
|
||||||
|
"Add BlockAdminMergeOverride to ProtectedBranch",
|
||||||
|
"Fix milestone deadline_unix when there is no due date",
|
||||||
|
"Add index(user_id, is_deleted) for action table",
|
||||||
|
}
|
||||||
|
|
||||||
|
// Version describes the version table. Should have only one row with id==1
|
||||||
|
type Version struct {
|
||||||
|
ID int64 `xorm:"pk autoincr"`
|
||||||
|
Version int64
|
||||||
|
}
|
||||||
|
|
||||||
|
func MigrateToXormigrate(x *xorm.Engine) error {
|
||||||
|
if err := x.Sync(new(Version)); err != nil {
|
||||||
|
return fmt.Errorf("sync: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
currentVersion := &Version{ID: 1}
|
||||||
|
has, err := x.Get(currentVersion)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("get: %w", err)
|
||||||
|
} else if !has {
|
||||||
|
// This should not happen
|
||||||
|
return fmt.Errorf("could not get version")
|
||||||
|
}
|
||||||
|
|
||||||
|
v := currentVersion.Version
|
||||||
|
if minDBVersion > v {
|
||||||
|
log.Fatal(`Gitea no longer supports auto-migration from your previously installed version.
|
||||||
|
Please try upgrading to i.e. v1.6.4 first, then upgrade to this version.`)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Downgrading Gitea's database version not supported
|
||||||
|
if int(v-minDBVersion) > oldMigrationsCount {
|
||||||
|
msg := fmt.Sprintf("Your database (migration version: %d) is for a newer Gitea, you can not use the newer database for this old Gitea release (%d).", v, expectedVersion)
|
||||||
|
msg += "\nGitea will exit to keep your database safe and unchanged. Please use the correct Gitea release, do not change the migration version manually (incorrect manual operation may lose data)."
|
||||||
|
if !setting.IsProd {
|
||||||
|
msg += fmt.Sprintf("\nIf you are in development and really know what you're doing, you can force changing the migration version by executing: UPDATE version SET version=%d WHERE id=1;", expectedVersion)
|
||||||
|
}
|
||||||
|
log.Fatal("Migration Error: %s", msg)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// add migrations that already have been run
|
||||||
|
for i := v; i < 307; i++ {
|
||||||
|
if _, err := x.Insert(&xormigrate.Migration{ID: fmt.Sprint(i)}); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove old version table
|
||||||
|
return x.DropTables(new(Version))
|
||||||
|
}
|
|
@ -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)
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,6 +7,7 @@ import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"html/template"
|
"html/template"
|
||||||
|
"maps"
|
||||||
"net"
|
"net"
|
||||||
"net/url"
|
"net/url"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
@ -165,10 +166,10 @@ type Repository struct {
|
||||||
|
|
||||||
Status RepositoryStatus `xorm:"NOT NULL DEFAULT 0"`
|
Status RepositoryStatus `xorm:"NOT NULL DEFAULT 0"`
|
||||||
|
|
||||||
RenderingMetas map[string]string `xorm:"-"`
|
commonRenderingMetas map[string]string `xorm:"-"`
|
||||||
DocumentRenderingMetas map[string]string `xorm:"-"`
|
|
||||||
Units []*RepoUnit `xorm:"-"`
|
Units []*RepoUnit `xorm:"-"`
|
||||||
PrimaryLanguage *LanguageStat `xorm:"-"`
|
PrimaryLanguage *LanguageStat `xorm:"-"`
|
||||||
|
|
||||||
IsFork bool `xorm:"INDEX NOT NULL DEFAULT false"`
|
IsFork bool `xorm:"INDEX NOT NULL DEFAULT false"`
|
||||||
ForkID int64 `xorm:"INDEX"`
|
ForkID int64 `xorm:"INDEX"`
|
||||||
|
@ -473,13 +474,11 @@ func (repo *Repository) MustOwner(ctx context.Context) *user_model.User {
|
||||||
return repo.Owner
|
return repo.Owner
|
||||||
}
|
}
|
||||||
|
|
||||||
// ComposeMetas composes a map of metas for properly rendering issue links and external issue trackers.
|
func (repo *Repository) composeCommonMetas(ctx context.Context) map[string]string {
|
||||||
func (repo *Repository) ComposeMetas(ctx context.Context) map[string]string {
|
if len(repo.commonRenderingMetas) == 0 {
|
||||||
if len(repo.RenderingMetas) == 0 {
|
|
||||||
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)
|
||||||
|
@ -509,22 +508,34 @@ func (repo *Repository) ComposeMetas(ctx context.Context) map[string]string {
|
||||||
metas["org"] = strings.ToLower(repo.OwnerName)
|
metas["org"] = strings.ToLower(repo.OwnerName)
|
||||||
}
|
}
|
||||||
|
|
||||||
repo.RenderingMetas = metas
|
repo.commonRenderingMetas = metas
|
||||||
}
|
}
|
||||||
return repo.RenderingMetas
|
return repo.commonRenderingMetas
|
||||||
}
|
}
|
||||||
|
|
||||||
// ComposeDocumentMetas composes a map of metas for properly rendering documents
|
// ComposeMetas composes a map of metas for properly rendering comments or comment-like contents (commit message)
|
||||||
|
func (repo *Repository) ComposeMetas(ctx context.Context) map[string]string {
|
||||||
|
metas := maps.Clone(repo.composeCommonMetas(ctx))
|
||||||
|
metas["markdownLineBreakStyle"] = "comment"
|
||||||
|
metas["markupAllowShortIssuePattern"] = "true"
|
||||||
|
return metas
|
||||||
|
}
|
||||||
|
|
||||||
|
// ComposeWikiMetas composes a map of metas for properly rendering wikis
|
||||||
|
func (repo *Repository) ComposeWikiMetas(ctx context.Context) map[string]string {
|
||||||
|
// does wiki need the "teams" and "org" from common metas?
|
||||||
|
metas := maps.Clone(repo.composeCommonMetas(ctx))
|
||||||
|
metas["markdownLineBreakStyle"] = "document"
|
||||||
|
metas["markupAllowShortIssuePattern"] = "true"
|
||||||
|
return metas
|
||||||
|
}
|
||||||
|
|
||||||
|
// ComposeDocumentMetas composes a map of metas for properly rendering documents (repo files)
|
||||||
func (repo *Repository) ComposeDocumentMetas(ctx context.Context) map[string]string {
|
func (repo *Repository) ComposeDocumentMetas(ctx context.Context) map[string]string {
|
||||||
if len(repo.DocumentRenderingMetas) == 0 {
|
// does document(file) need the "teams" and "org" from common metas?
|
||||||
metas := map[string]string{}
|
metas := maps.Clone(repo.composeCommonMetas(ctx))
|
||||||
for k, v := range repo.ComposeMetas(ctx) {
|
metas["markdownLineBreakStyle"] = "document"
|
||||||
metas[k] = v
|
return metas
|
||||||
}
|
|
||||||
metas["mode"] = "document"
|
|
||||||
repo.DocumentRenderingMetas = metas
|
|
||||||
}
|
|
||||||
return repo.DocumentRenderingMetas
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetBaseRepo populates repo.BaseRepo for a fork repository and
|
// GetBaseRepo populates repo.BaseRepo for a fork repository and
|
||||||
|
|
|
@ -1,13 +1,12 @@
|
||||||
// Copyright 2017 The Gitea Authors. All rights reserved.
|
// Copyright 2017 The Gitea Authors. All rights reserved.
|
||||||
// SPDX-License-Identifier: MIT
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
package repo_test
|
package repo
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"code.gitea.io/gitea/models/db"
|
"code.gitea.io/gitea/models/db"
|
||||||
repo_model "code.gitea.io/gitea/models/repo"
|
|
||||||
"code.gitea.io/gitea/models/unit"
|
"code.gitea.io/gitea/models/unit"
|
||||||
"code.gitea.io/gitea/models/unittest"
|
"code.gitea.io/gitea/models/unittest"
|
||||||
user_model "code.gitea.io/gitea/models/user"
|
user_model "code.gitea.io/gitea/models/user"
|
||||||
|
@ -20,18 +19,18 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
countRepospts = repo_model.CountRepositoryOptions{OwnerID: 10}
|
countRepospts = CountRepositoryOptions{OwnerID: 10}
|
||||||
countReposptsPublic = repo_model.CountRepositoryOptions{OwnerID: 10, Private: optional.Some(false)}
|
countReposptsPublic = CountRepositoryOptions{OwnerID: 10, Private: optional.Some(false)}
|
||||||
countReposptsPrivate = repo_model.CountRepositoryOptions{OwnerID: 10, Private: optional.Some(true)}
|
countReposptsPrivate = CountRepositoryOptions{OwnerID: 10, Private: optional.Some(true)}
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestGetRepositoryCount(t *testing.T) {
|
func TestGetRepositoryCount(t *testing.T) {
|
||||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||||
|
|
||||||
ctx := db.DefaultContext
|
ctx := db.DefaultContext
|
||||||
count, err1 := repo_model.CountRepositories(ctx, countRepospts)
|
count, err1 := CountRepositories(ctx, countRepospts)
|
||||||
privateCount, err2 := repo_model.CountRepositories(ctx, countReposptsPrivate)
|
privateCount, err2 := CountRepositories(ctx, countReposptsPrivate)
|
||||||
publicCount, err3 := repo_model.CountRepositories(ctx, countReposptsPublic)
|
publicCount, err3 := CountRepositories(ctx, countReposptsPublic)
|
||||||
assert.NoError(t, err1)
|
assert.NoError(t, err1)
|
||||||
assert.NoError(t, err2)
|
assert.NoError(t, err2)
|
||||||
assert.NoError(t, err3)
|
assert.NoError(t, err3)
|
||||||
|
@ -42,7 +41,7 @@ func TestGetRepositoryCount(t *testing.T) {
|
||||||
func TestGetPublicRepositoryCount(t *testing.T) {
|
func TestGetPublicRepositoryCount(t *testing.T) {
|
||||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||||
|
|
||||||
count, err := repo_model.CountRepositories(db.DefaultContext, countReposptsPublic)
|
count, err := CountRepositories(db.DefaultContext, countReposptsPublic)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
assert.Equal(t, int64(1), count)
|
assert.Equal(t, int64(1), count)
|
||||||
}
|
}
|
||||||
|
@ -50,14 +49,14 @@ func TestGetPublicRepositoryCount(t *testing.T) {
|
||||||
func TestGetPrivateRepositoryCount(t *testing.T) {
|
func TestGetPrivateRepositoryCount(t *testing.T) {
|
||||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||||
|
|
||||||
count, err := repo_model.CountRepositories(db.DefaultContext, countReposptsPrivate)
|
count, err := CountRepositories(db.DefaultContext, countReposptsPrivate)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
assert.Equal(t, int64(2), count)
|
assert.Equal(t, int64(2), count)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestRepoAPIURL(t *testing.T) {
|
func TestRepoAPIURL(t *testing.T) {
|
||||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||||
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 10})
|
repo := unittest.AssertExistsAndLoadBean(t, &Repository{ID: 10})
|
||||||
|
|
||||||
assert.Equal(t, "https://try.gitea.io/api/v1/repos/user12/repo10", repo.APIURL())
|
assert.Equal(t, "https://try.gitea.io/api/v1/repos/user12/repo10", repo.APIURL())
|
||||||
}
|
}
|
||||||
|
@ -65,22 +64,22 @@ func TestRepoAPIURL(t *testing.T) {
|
||||||
func TestWatchRepo(t *testing.T) {
|
func TestWatchRepo(t *testing.T) {
|
||||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||||
|
|
||||||
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 3})
|
repo := unittest.AssertExistsAndLoadBean(t, &Repository{ID: 3})
|
||||||
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
|
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
|
||||||
|
|
||||||
assert.NoError(t, repo_model.WatchRepo(db.DefaultContext, user, repo, true))
|
assert.NoError(t, WatchRepo(db.DefaultContext, user, repo, true))
|
||||||
unittest.AssertExistsAndLoadBean(t, &repo_model.Watch{RepoID: repo.ID, UserID: user.ID})
|
unittest.AssertExistsAndLoadBean(t, &Watch{RepoID: repo.ID, UserID: user.ID})
|
||||||
unittest.CheckConsistencyFor(t, &repo_model.Repository{ID: repo.ID})
|
unittest.CheckConsistencyFor(t, &Repository{ID: repo.ID})
|
||||||
|
|
||||||
assert.NoError(t, repo_model.WatchRepo(db.DefaultContext, user, repo, false))
|
assert.NoError(t, WatchRepo(db.DefaultContext, user, repo, false))
|
||||||
unittest.AssertNotExistsBean(t, &repo_model.Watch{RepoID: repo.ID, UserID: user.ID})
|
unittest.AssertNotExistsBean(t, &Watch{RepoID: repo.ID, UserID: user.ID})
|
||||||
unittest.CheckConsistencyFor(t, &repo_model.Repository{ID: repo.ID})
|
unittest.CheckConsistencyFor(t, &Repository{ID: repo.ID})
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestMetas(t *testing.T) {
|
func TestMetas(t *testing.T) {
|
||||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||||
|
|
||||||
repo := &repo_model.Repository{Name: "testRepo"}
|
repo := &Repository{Name: "testRepo"}
|
||||||
repo.Owner = &user_model.User{Name: "testOwner"}
|
repo.Owner = &user_model.User{Name: "testOwner"}
|
||||||
repo.OwnerName = repo.Owner.Name
|
repo.OwnerName = repo.Owner.Name
|
||||||
|
|
||||||
|
@ -90,16 +89,16 @@ func TestMetas(t *testing.T) {
|
||||||
assert.Equal(t, "testRepo", metas["repo"])
|
assert.Equal(t, "testRepo", metas["repo"])
|
||||||
assert.Equal(t, "testOwner", metas["user"])
|
assert.Equal(t, "testOwner", metas["user"])
|
||||||
|
|
||||||
externalTracker := repo_model.RepoUnit{
|
externalTracker := RepoUnit{
|
||||||
Type: unit.TypeExternalTracker,
|
Type: unit.TypeExternalTracker,
|
||||||
Config: &repo_model.ExternalTrackerConfig{
|
Config: &ExternalTrackerConfig{
|
||||||
ExternalTrackerFormat: "https://someurl.com/{user}/{repo}/{issue}",
|
ExternalTrackerFormat: "https://someurl.com/{user}/{repo}/{issue}",
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
testSuccess := func(expectedStyle string) {
|
testSuccess := func(expectedStyle string) {
|
||||||
repo.Units = []*repo_model.RepoUnit{&externalTracker}
|
repo.Units = []*RepoUnit{&externalTracker}
|
||||||
repo.RenderingMetas = nil
|
repo.commonRenderingMetas = nil
|
||||||
metas := repo.ComposeMetas(db.DefaultContext)
|
metas := repo.ComposeMetas(db.DefaultContext)
|
||||||
assert.Equal(t, expectedStyle, metas["style"])
|
assert.Equal(t, expectedStyle, metas["style"])
|
||||||
assert.Equal(t, "testRepo", metas["repo"])
|
assert.Equal(t, "testRepo", metas["repo"])
|
||||||
|
@ -118,7 +117,7 @@ func TestMetas(t *testing.T) {
|
||||||
externalTracker.ExternalTrackerConfig().ExternalTrackerStyle = markup.IssueNameStyleRegexp
|
externalTracker.ExternalTrackerConfig().ExternalTrackerStyle = markup.IssueNameStyleRegexp
|
||||||
testSuccess(markup.IssueNameStyleRegexp)
|
testSuccess(markup.IssueNameStyleRegexp)
|
||||||
|
|
||||||
repo, err := repo_model.GetRepositoryByID(db.DefaultContext, 3)
|
repo, err := GetRepositoryByID(db.DefaultContext, 3)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
|
|
||||||
metas = repo.ComposeMetas(db.DefaultContext)
|
metas = repo.ComposeMetas(db.DefaultContext)
|
||||||
|
@ -132,7 +131,7 @@ func TestGetRepositoryByURL(t *testing.T) {
|
||||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||||
|
|
||||||
t.Run("InvalidPath", func(t *testing.T) {
|
t.Run("InvalidPath", func(t *testing.T) {
|
||||||
repo, err := repo_model.GetRepositoryByURL(db.DefaultContext, "something")
|
repo, err := GetRepositoryByURL(db.DefaultContext, "something")
|
||||||
|
|
||||||
assert.Nil(t, repo)
|
assert.Nil(t, repo)
|
||||||
assert.Error(t, err)
|
assert.Error(t, err)
|
||||||
|
@ -140,7 +139,7 @@ func TestGetRepositoryByURL(t *testing.T) {
|
||||||
|
|
||||||
t.Run("ValidHttpURL", func(t *testing.T) {
|
t.Run("ValidHttpURL", func(t *testing.T) {
|
||||||
test := func(t *testing.T, url string) {
|
test := func(t *testing.T, url string) {
|
||||||
repo, err := repo_model.GetRepositoryByURL(db.DefaultContext, url)
|
repo, err := GetRepositoryByURL(db.DefaultContext, url)
|
||||||
|
|
||||||
assert.NotNil(t, repo)
|
assert.NotNil(t, repo)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
|
@ -155,7 +154,7 @@ func TestGetRepositoryByURL(t *testing.T) {
|
||||||
|
|
||||||
t.Run("ValidGitSshURL", func(t *testing.T) {
|
t.Run("ValidGitSshURL", func(t *testing.T) {
|
||||||
test := func(t *testing.T, url string) {
|
test := func(t *testing.T, url string) {
|
||||||
repo, err := repo_model.GetRepositoryByURL(db.DefaultContext, url)
|
repo, err := GetRepositoryByURL(db.DefaultContext, url)
|
||||||
|
|
||||||
assert.NotNil(t, repo)
|
assert.NotNil(t, repo)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
|
@ -173,7 +172,7 @@ func TestGetRepositoryByURL(t *testing.T) {
|
||||||
|
|
||||||
t.Run("ValidImplicitSshURL", func(t *testing.T) {
|
t.Run("ValidImplicitSshURL", func(t *testing.T) {
|
||||||
test := func(t *testing.T, url string) {
|
test := func(t *testing.T, url string) {
|
||||||
repo, err := repo_model.GetRepositoryByURL(db.DefaultContext, url)
|
repo, err := GetRepositoryByURL(db.DefaultContext, url)
|
||||||
|
|
||||||
assert.NotNil(t, repo)
|
assert.NotNil(t, repo)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
|
@ -200,21 +199,21 @@ func TestComposeSSHCloneURL(t *testing.T) {
|
||||||
setting.SSH.Domain = "domain"
|
setting.SSH.Domain = "domain"
|
||||||
setting.SSH.Port = 22
|
setting.SSH.Port = 22
|
||||||
setting.Repository.UseCompatSSHURI = false
|
setting.Repository.UseCompatSSHURI = false
|
||||||
assert.Equal(t, "git@domain:user/repo.git", repo_model.ComposeSSHCloneURL("user", "repo"))
|
assert.Equal(t, "git@domain:user/repo.git", ComposeSSHCloneURL("user", "repo"))
|
||||||
setting.Repository.UseCompatSSHURI = true
|
setting.Repository.UseCompatSSHURI = true
|
||||||
assert.Equal(t, "ssh://git@domain/user/repo.git", repo_model.ComposeSSHCloneURL("user", "repo"))
|
assert.Equal(t, "ssh://git@domain/user/repo.git", ComposeSSHCloneURL("user", "repo"))
|
||||||
// test SSH_DOMAIN while use non-standard SSH port
|
// test SSH_DOMAIN while use non-standard SSH port
|
||||||
setting.SSH.Port = 123
|
setting.SSH.Port = 123
|
||||||
setting.Repository.UseCompatSSHURI = false
|
setting.Repository.UseCompatSSHURI = false
|
||||||
assert.Equal(t, "ssh://git@domain:123/user/repo.git", repo_model.ComposeSSHCloneURL("user", "repo"))
|
assert.Equal(t, "ssh://git@domain:123/user/repo.git", ComposeSSHCloneURL("user", "repo"))
|
||||||
setting.Repository.UseCompatSSHURI = true
|
setting.Repository.UseCompatSSHURI = true
|
||||||
assert.Equal(t, "ssh://git@domain:123/user/repo.git", repo_model.ComposeSSHCloneURL("user", "repo"))
|
assert.Equal(t, "ssh://git@domain:123/user/repo.git", ComposeSSHCloneURL("user", "repo"))
|
||||||
|
|
||||||
// test IPv6 SSH_DOMAIN
|
// test IPv6 SSH_DOMAIN
|
||||||
setting.Repository.UseCompatSSHURI = false
|
setting.Repository.UseCompatSSHURI = false
|
||||||
setting.SSH.Domain = "::1"
|
setting.SSH.Domain = "::1"
|
||||||
setting.SSH.Port = 22
|
setting.SSH.Port = 22
|
||||||
assert.Equal(t, "git@[::1]:user/repo.git", repo_model.ComposeSSHCloneURL("user", "repo"))
|
assert.Equal(t, "git@[::1]:user/repo.git", ComposeSSHCloneURL("user", "repo"))
|
||||||
setting.SSH.Port = 123
|
setting.SSH.Port = 123
|
||||||
assert.Equal(t, "ssh://git@[::1]:123/user/repo.git", repo_model.ComposeSSHCloneURL("user", "repo"))
|
assert.Equal(t, "ssh://git@[::1]:123/user/repo.git", ComposeSSHCloneURL("user", "repo"))
|
||||||
}
|
}
|
||||||
|
|
|
@ -36,6 +36,7 @@ var OrderByMap = map[string]map[string]db.SearchOrderBy{
|
||||||
var OrderByFlatMap = map[string]db.SearchOrderBy{
|
var OrderByFlatMap = map[string]db.SearchOrderBy{
|
||||||
"newest": OrderByMap["desc"]["created"],
|
"newest": OrderByMap["desc"]["created"],
|
||||||
"oldest": OrderByMap["asc"]["created"],
|
"oldest": OrderByMap["asc"]["created"],
|
||||||
|
"recentupdate": OrderByMap["desc"]["updated"],
|
||||||
"leastupdate": OrderByMap["asc"]["updated"],
|
"leastupdate": OrderByMap["asc"]["updated"],
|
||||||
"reversealphabetically": OrderByMap["desc"]["alpha"],
|
"reversealphabetically": OrderByMap["desc"]["alpha"],
|
||||||
"alphabetically": OrderByMap["asc"]["alpha"],
|
"alphabetically": OrderByMap["asc"]["alpha"],
|
||||||
|
|
|
@ -4,10 +4,8 @@
|
||||||
package unittest
|
package unittest
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
|
||||||
"io"
|
|
||||||
"os"
|
"os"
|
||||||
"path"
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"code.gitea.io/gitea/modules/util"
|
"code.gitea.io/gitea/modules/util"
|
||||||
|
@ -32,67 +30,73 @@ func Copy(src, dest string) error {
|
||||||
return os.Symlink(target, dest)
|
return os.Symlink(target, dest)
|
||||||
}
|
}
|
||||||
|
|
||||||
sr, err := os.Open(src)
|
return util.CopyFile(src, dest)
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
defer sr.Close()
|
|
||||||
|
|
||||||
dw, err := os.Create(dest)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
defer dw.Close()
|
|
||||||
|
|
||||||
if _, err = io.Copy(dw, sr); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set back file information.
|
|
||||||
if err = os.Chtimes(dest, si.ModTime(), si.ModTime()); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return os.Chmod(dest, si.Mode())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// CopyDir copy files recursively from source to target directory.
|
// Sync synchronizes the two files. This is skipped if both files
|
||||||
//
|
// exist and the size, modtime, and mode match.
|
||||||
// The filter accepts a function that process the path info.
|
func Sync(srcPath, destPath string) error {
|
||||||
// and should return true for need to filter.
|
dest, err := os.Stat(destPath)
|
||||||
//
|
if err != nil {
|
||||||
// It returns error when error occurs in underlying functions.
|
if os.IsNotExist(err) {
|
||||||
func CopyDir(srcPath, destPath string, filters ...func(filePath string) bool) error {
|
return Copy(srcPath, destPath)
|
||||||
// Check if target directory exists.
|
}
|
||||||
if _, err := os.Stat(destPath); !errors.Is(err, os.ErrNotExist) {
|
return err
|
||||||
return util.NewAlreadyExistErrorf("file or directory already exists: %s", destPath)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
src, err := os.Stat(srcPath)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if src.Size() == dest.Size() &&
|
||||||
|
src.ModTime() == dest.ModTime() &&
|
||||||
|
src.Mode() == dest.Mode() {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return Copy(srcPath, destPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SyncDirs synchronizes files recursively from source to target directory.
|
||||||
|
// It returns error when error occurs in underlying functions.
|
||||||
|
func SyncDirs(srcPath, destPath string) error {
|
||||||
err := os.MkdirAll(destPath, os.ModePerm)
|
err := os.MkdirAll(destPath, os.ModePerm)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Gather directory info.
|
// find and delete all untracked files
|
||||||
infos, err := util.StatDir(srcPath, true)
|
destFiles, err := util.StatDir(destPath, true)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
for _, destFile := range destFiles {
|
||||||
var filter func(filePath string) bool
|
destFilePath := filepath.Join(destPath, destFile)
|
||||||
if len(filters) > 0 {
|
if _, err = os.Stat(filepath.Join(srcPath, destFile)); err != nil {
|
||||||
filter = filters[0]
|
if os.IsNotExist(err) {
|
||||||
|
// if src file does not exist, remove dest file
|
||||||
|
if err = os.RemoveAll(destFilePath); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, info := range infos {
|
// sync src files to dest
|
||||||
if filter != nil && filter(info) {
|
srcFiles, err := util.StatDir(srcPath, true)
|
||||||
continue
|
if err != nil {
|
||||||
}
|
return err
|
||||||
|
}
|
||||||
curPath := path.Join(destPath, info)
|
for _, srcFile := range srcFiles {
|
||||||
if strings.HasSuffix(info, "/") {
|
destFilePath := filepath.Join(destPath, srcFile)
|
||||||
err = os.MkdirAll(curPath, os.ModePerm)
|
// util.StatDir appends a slash to the directory name
|
||||||
|
if strings.HasSuffix(srcFile, "/") {
|
||||||
|
err = os.MkdirAll(destFilePath, os.ModePerm)
|
||||||
} else {
|
} else {
|
||||||
err = Copy(path.Join(srcPath, info), curPath)
|
err = Sync(filepath.Join(srcPath, srcFile), destFilePath)
|
||||||
}
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
|
|
@ -164,35 +164,13 @@ func MainTest(m *testing.M, testOpts ...*TestOptions) {
|
||||||
if err = storage.Init(); err != nil {
|
if err = storage.Init(); err != nil {
|
||||||
fatalTestError("storage.Init: %v\n", err)
|
fatalTestError("storage.Init: %v\n", err)
|
||||||
}
|
}
|
||||||
if err = util.RemoveAll(repoRootPath); err != nil {
|
if err = SyncDirs(filepath.Join(giteaRoot, "tests", "gitea-repositories-meta"), setting.RepoRootPath); err != nil {
|
||||||
fatalTestError("util.RemoveAll: %v\n", err)
|
fatalTestError("util.SyncDirs: %v\n", err)
|
||||||
}
|
|
||||||
if err = CopyDir(filepath.Join(giteaRoot, "tests", "gitea-repositories-meta"), setting.RepoRootPath); err != nil {
|
|
||||||
fatalTestError("util.CopyDir: %v\n", err)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if err = git.InitFull(context.Background()); err != nil {
|
if err = git.InitFull(context.Background()); err != nil {
|
||||||
fatalTestError("git.Init: %v\n", err)
|
fatalTestError("git.Init: %v\n", err)
|
||||||
}
|
}
|
||||||
ownerDirs, err := os.ReadDir(setting.RepoRootPath)
|
|
||||||
if err != nil {
|
|
||||||
fatalTestError("unable to read the new repo root: %v\n", err)
|
|
||||||
}
|
|
||||||
for _, ownerDir := range ownerDirs {
|
|
||||||
if !ownerDir.Type().IsDir() {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
repoDirs, err := os.ReadDir(filepath.Join(setting.RepoRootPath, ownerDir.Name()))
|
|
||||||
if err != nil {
|
|
||||||
fatalTestError("unable to read the new repo root: %v\n", err)
|
|
||||||
}
|
|
||||||
for _, repoDir := range repoDirs {
|
|
||||||
_ = os.MkdirAll(filepath.Join(setting.RepoRootPath, ownerDir.Name(), repoDir.Name(), "objects", "pack"), 0o755)
|
|
||||||
_ = os.MkdirAll(filepath.Join(setting.RepoRootPath, ownerDir.Name(), repoDir.Name(), "objects", "info"), 0o755)
|
|
||||||
_ = os.MkdirAll(filepath.Join(setting.RepoRootPath, ownerDir.Name(), repoDir.Name(), "refs", "heads"), 0o755)
|
|
||||||
_ = os.MkdirAll(filepath.Join(setting.RepoRootPath, ownerDir.Name(), repoDir.Name(), "refs", "tag"), 0o755)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(testOpts) > 0 && testOpts[0].SetUp != nil {
|
if len(testOpts) > 0 && testOpts[0].SetUp != nil {
|
||||||
if err := testOpts[0].SetUp(); err != nil {
|
if err := testOpts[0].SetUp(); err != nil {
|
||||||
|
@ -255,24 +233,7 @@ func PrepareTestDatabase() error {
|
||||||
// by tests that use the above MainTest(..) function.
|
// by tests that use the above MainTest(..) function.
|
||||||
func PrepareTestEnv(t testing.TB) {
|
func PrepareTestEnv(t testing.TB) {
|
||||||
assert.NoError(t, PrepareTestDatabase())
|
assert.NoError(t, PrepareTestDatabase())
|
||||||
assert.NoError(t, util.RemoveAll(setting.RepoRootPath))
|
|
||||||
metaPath := filepath.Join(giteaRoot, "tests", "gitea-repositories-meta")
|
metaPath := filepath.Join(giteaRoot, "tests", "gitea-repositories-meta")
|
||||||
assert.NoError(t, CopyDir(metaPath, setting.RepoRootPath))
|
assert.NoError(t, SyncDirs(metaPath, setting.RepoRootPath))
|
||||||
ownerDirs, err := os.ReadDir(setting.RepoRootPath)
|
|
||||||
assert.NoError(t, err)
|
|
||||||
for _, ownerDir := range ownerDirs {
|
|
||||||
if !ownerDir.Type().IsDir() {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
repoDirs, err := os.ReadDir(filepath.Join(setting.RepoRootPath, ownerDir.Name()))
|
|
||||||
assert.NoError(t, err)
|
|
||||||
for _, repoDir := range repoDirs {
|
|
||||||
_ = os.MkdirAll(filepath.Join(setting.RepoRootPath, ownerDir.Name(), repoDir.Name(), "objects", "pack"), 0o755)
|
|
||||||
_ = os.MkdirAll(filepath.Join(setting.RepoRootPath, ownerDir.Name(), repoDir.Name(), "objects", "info"), 0o755)
|
|
||||||
_ = os.MkdirAll(filepath.Join(setting.RepoRootPath, ownerDir.Name(), repoDir.Name(), "refs", "heads"), 0o755)
|
|
||||||
_ = os.MkdirAll(filepath.Join(setting.RepoRootPath, ownerDir.Name(), repoDir.Name(), "refs", "tag"), 0o755)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
base.SetupGiteaRoot() // Makes sure GITEA_ROOT is set
|
base.SetupGiteaRoot() // Makes sure GITEA_ROOT is set
|
||||||
}
|
}
|
||||||
|
|
|
@ -1 +0,0 @@
|
||||||
Unnamed repository; edit this file 'description' to name the repository.
|
|
|
@ -1,6 +0,0 @@
|
||||||
# git ls-files --others --exclude-from=.git/info/exclude
|
|
||||||
# Lines that start with '#' are comments.
|
|
||||||
# For a project mostly in C, the following would be a good set of
|
|
||||||
# exclude patterns (uncomment them if you want to use them):
|
|
||||||
# *.[oa]
|
|
||||||
# *~
|
|
|
@ -1 +0,0 @@
|
||||||
Unnamed repository; edit this file 'description' to name the repository.
|
|
|
@ -1,6 +0,0 @@
|
||||||
# git ls-files --others --exclude-from=.git/info/exclude
|
|
||||||
# Lines that start with '#' are comments.
|
|
||||||
# For a project mostly in C, the following would be a good set of
|
|
||||||
# exclude patterns (uncomment them if you want to use them):
|
|
||||||
# *.[oa]
|
|
||||||
# *~
|
|
|
@ -1 +0,0 @@
|
||||||
Unnamed repository; edit this file 'description' to name the repository.
|
|
|
@ -1,6 +0,0 @@
|
||||||
# git ls-files --others --exclude-from=.git/info/exclude
|
|
||||||
# Lines that start with '#' are comments.
|
|
||||||
# For a project mostly in C, the following would be a good set of
|
|
||||||
# exclude patterns (uncomment them if you want to use them):
|
|
||||||
# *.[oa]
|
|
||||||
# *~
|
|
|
@ -1 +0,0 @@
|
||||||
Unnamed repository; edit this file 'description' to name the repository.
|
|
|
@ -1,6 +0,0 @@
|
||||||
# git ls-files --others --exclude-from=.git/info/exclude
|
|
||||||
# Lines that start with '#' are comments.
|
|
||||||
# For a project mostly in C, the following would be a good set of
|
|
||||||
# exclude patterns (uncomment them if you want to use them):
|
|
||||||
# *.[oa]
|
|
||||||
# *~
|
|
|
@ -1 +0,0 @@
|
||||||
Unnamed repository; edit this file 'description' to name the repository.
|
|
|
@ -1 +0,0 @@
|
||||||
Unnamed repository; edit this file 'description' to name the repository.
|
|
|
@ -1,6 +0,0 @@
|
||||||
# git ls-files --others --exclude-from=.git/info/exclude
|
|
||||||
# Lines that start with '#' are comments.
|
|
||||||
# For a project mostly in C, the following would be a good set of
|
|
||||||
# exclude patterns (uncomment them if you want to use them):
|
|
||||||
# *.[oa]
|
|
||||||
# *~
|
|
|
@ -1 +0,0 @@
|
||||||
Unnamed repository; edit this file 'description' to name the repository.
|
|
|
@ -1 +0,0 @@
|
||||||
Unnamed repository; edit this file 'description' to name the repository.
|
|
|
@ -1,6 +0,0 @@
|
||||||
# git ls-files --others --exclude-from=.git/info/exclude
|
|
||||||
# Lines that start with '#' are comments.
|
|
||||||
# For a project mostly in C, the following would be a good set of
|
|
||||||
# exclude patterns (uncomment them if you want to use them):
|
|
||||||
# *.[oa]
|
|
||||||
# *~
|
|
|
@ -1 +0,0 @@
|
||||||
Unnamed repository; edit this file 'description' to name the repository.
|
|
|
@ -1,6 +0,0 @@
|
||||||
# git ls-files --others --exclude-from=.git/info/exclude
|
|
||||||
# Lines that start with '#' are comments.
|
|
||||||
# For a project mostly in C, the following would be a good set of
|
|
||||||
# exclude patterns (uncomment them if you want to use them):
|
|
||||||
# *.[oa]
|
|
||||||
# *~
|
|
|
@ -46,7 +46,7 @@ func ServeSetHeaders(w http.ResponseWriter, opts *ServeHeaderOptions) {
|
||||||
w.Header().Add(gzhttp.HeaderNoCompression, "1")
|
w.Header().Add(gzhttp.HeaderNoCompression, "1")
|
||||||
}
|
}
|
||||||
|
|
||||||
contentType := typesniffer.ApplicationOctetStream
|
contentType := typesniffer.MimeTypeApplicationOctetStream
|
||||||
if opts.ContentType != "" {
|
if opts.ContentType != "" {
|
||||||
if opts.ContentTypeCharset != "" {
|
if opts.ContentTypeCharset != "" {
|
||||||
contentType = opts.ContentType + "; charset=" + strings.ToLower(opts.ContentTypeCharset)
|
contentType = opts.ContentType + "; charset=" + strings.ToLower(opts.ContentTypeCharset)
|
||||||
|
@ -107,7 +107,7 @@ func setServeHeadersByFile(r *http.Request, w http.ResponseWriter, filePath stri
|
||||||
} else if isPlain {
|
} else if isPlain {
|
||||||
opts.ContentType = "text/plain"
|
opts.ContentType = "text/plain"
|
||||||
} else {
|
} else {
|
||||||
opts.ContentType = typesniffer.ApplicationOctetStream
|
opts.ContentType = typesniffer.MimeTypeApplicationOctetStream
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -21,6 +21,7 @@ import (
|
||||||
_ "code.gitea.io/gitea/models/activities"
|
_ "code.gitea.io/gitea/models/activities"
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
|
||||||
_ "github.com/mattn/go-sqlite3"
|
_ "github.com/mattn/go-sqlite3"
|
||||||
)
|
)
|
||||||
|
@ -284,15 +285,11 @@ func TestBleveIndexAndSearch(t *testing.T) {
|
||||||
dir := t.TempDir()
|
dir := t.TempDir()
|
||||||
|
|
||||||
idx := bleve.NewIndexer(dir)
|
idx := bleve.NewIndexer(dir)
|
||||||
_, err := idx.Init(context.Background())
|
|
||||||
if err != nil {
|
|
||||||
if idx != nil {
|
|
||||||
idx.Close()
|
|
||||||
}
|
|
||||||
assert.FailNow(t, "Unable to create bleve indexer Error: %v", err)
|
|
||||||
}
|
|
||||||
defer idx.Close()
|
defer idx.Close()
|
||||||
|
|
||||||
|
_, err := idx.Init(context.Background())
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
testIndexer("beleve", t, idx)
|
testIndexer("beleve", t, idx)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -86,6 +86,8 @@ type ColoredValue struct {
|
||||||
colors []ColorAttribute
|
colors []ColorAttribute
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var _ fmt.Formatter = (*ColoredValue)(nil)
|
||||||
|
|
||||||
func (c *ColoredValue) Format(f fmt.State, verb rune) {
|
func (c *ColoredValue) Format(f fmt.State, verb rune) {
|
||||||
_, _ = f.Write(ColorBytes(c.colors...))
|
_, _ = f.Write(ColorBytes(c.colors...))
|
||||||
s := fmt.Sprintf(fmt.FormatString(f, verb), c.v)
|
s := fmt.Sprintf(fmt.FormatString(f, verb), c.v)
|
||||||
|
@ -93,6 +95,10 @@ func (c *ColoredValue) Format(f fmt.State, verb rune) {
|
||||||
_, _ = f.Write(resetBytes)
|
_, _ = f.Write(resetBytes)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *ColoredValue) Value() any {
|
||||||
|
return c.v
|
||||||
|
}
|
||||||
|
|
||||||
func NewColoredValue(v any, color ...ColorAttribute) *ColoredValue {
|
func NewColoredValue(v any, color ...ColorAttribute) *ColoredValue {
|
||||||
return &ColoredValue{v: v, colors: color}
|
return &ColoredValue{v: v, colors: color}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
|
||||||
}
|
|
||||||
|
|
|
@ -7,11 +7,11 @@ import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"io"
|
"io"
|
||||||
"regexp"
|
"regexp"
|
||||||
|
"slices"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
"code.gitea.io/gitea/modules/markup/common"
|
"code.gitea.io/gitea/modules/markup/common"
|
||||||
"code.gitea.io/gitea/modules/setting"
|
|
||||||
|
|
||||||
"golang.org/x/net/html"
|
"golang.org/x/net/html"
|
||||||
"golang.org/x/net/html/atom"
|
"golang.org/x/net/html/atom"
|
||||||
|
@ -25,7 +25,27 @@ const (
|
||||||
IssueNameStyleRegexp = "regexp"
|
IssueNameStyleRegexp = "regexp"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
// CSS class for action keywords (e.g. "closes: #1")
|
||||||
|
const keywordClass = "issue-keyword"
|
||||||
|
|
||||||
|
type globalVarsType struct {
|
||||||
|
hashCurrentPattern *regexp.Regexp
|
||||||
|
shortLinkPattern *regexp.Regexp
|
||||||
|
anyHashPattern *regexp.Regexp
|
||||||
|
comparePattern *regexp.Regexp
|
||||||
|
fullURLPattern *regexp.Regexp
|
||||||
|
emailRegex *regexp.Regexp
|
||||||
|
blackfridayExtRegex *regexp.Regexp
|
||||||
|
emojiShortCodeRegex *regexp.Regexp
|
||||||
|
issueFullPattern *regexp.Regexp
|
||||||
|
filesChangedFullPattern *regexp.Regexp
|
||||||
|
|
||||||
|
tagCleaner *regexp.Regexp
|
||||||
|
nulCleaner *strings.Replacer
|
||||||
|
}
|
||||||
|
|
||||||
|
var globalVars = sync.OnceValue[*globalVarsType](func() *globalVarsType {
|
||||||
|
v := &globalVarsType{}
|
||||||
// NOTE: All below regex matching do not perform any extra validation.
|
// NOTE: All below regex matching do not perform any extra validation.
|
||||||
// Thus a link is produced even if the linked entity does not exist.
|
// Thus a link is produced even if the linked entity does not exist.
|
||||||
// While fast, this is also incorrect and lead to false positives.
|
// While fast, this is also incorrect and lead to false positives.
|
||||||
|
@ -36,79 +56,56 @@ var (
|
||||||
// hashCurrentPattern matches string that represents a commit SHA, e.g. d8a994ef243349f321568f9e36d5c3f444b99cae
|
// hashCurrentPattern matches string that represents a commit SHA, e.g. d8a994ef243349f321568f9e36d5c3f444b99cae
|
||||||
// Although SHA1 hashes are 40 chars long, SHA256 are 64, the regex matches the hash from 7 to 64 chars in length
|
// Although SHA1 hashes are 40 chars long, SHA256 are 64, the regex matches the hash from 7 to 64 chars in length
|
||||||
// so that abbreviated hash links can be used as well. This matches git and GitHub usability.
|
// so that abbreviated hash links can be used as well. This matches git and GitHub usability.
|
||||||
hashCurrentPattern = regexp.MustCompile(`(?:\s|^|\(|\[)([0-9a-f]{7,64})(?:\s|$|\)|\]|[.,:](\s|$))`)
|
v.hashCurrentPattern = regexp.MustCompile(`(?:\s|^|\(|\[)([0-9a-f]{7,64})(?:\s|$|\)|\]|[.,:](\s|$))`)
|
||||||
|
|
||||||
// shortLinkPattern matches short but difficult to parse [[name|link|arg=test]] syntax
|
// shortLinkPattern matches short but difficult to parse [[name|link|arg=test]] syntax
|
||||||
shortLinkPattern = regexp.MustCompile(`\[\[(.*?)\]\](\w*)`)
|
v.shortLinkPattern = regexp.MustCompile(`\[\[(.*?)\]\](\w*)`)
|
||||||
|
|
||||||
// anyHashPattern splits url containing SHA into parts
|
// anyHashPattern splits url containing SHA into parts
|
||||||
anyHashPattern = regexp.MustCompile(`https?://(?:\S+/){4,5}([0-9a-f]{40,64})(/[-+~%./\w]+)?(\?[-+~%.\w&=]+)?(#[-+~%.\w]+)?`)
|
v.anyHashPattern = regexp.MustCompile(`https?://(?:\S+/){4,5}([0-9a-f]{40,64})(/[-+~%./\w]+)?(\?[-+~%.\w&=]+)?(#[-+~%.\w]+)?`)
|
||||||
|
|
||||||
// comparePattern matches "http://domain/org/repo/compare/COMMIT1...COMMIT2#hash"
|
// comparePattern matches "http://domain/org/repo/compare/COMMIT1...COMMIT2#hash"
|
||||||
comparePattern = regexp.MustCompile(`https?://(?:\S+/){4,5}([0-9a-f]{7,64})(\.\.\.?)([0-9a-f]{7,64})?(#[-+~_%.a-zA-Z0-9]+)?`)
|
v.comparePattern = regexp.MustCompile(`https?://(?:\S+/){4,5}([0-9a-f]{7,64})(\.\.\.?)([0-9a-f]{7,64})?(#[-+~_%.a-zA-Z0-9]+)?`)
|
||||||
|
|
||||||
// fullURLPattern matches full URL like "mailto:...", "https://..." and "ssh+git://..."
|
// fullURLPattern matches full URL like "mailto:...", "https://..." and "ssh+git://..."
|
||||||
fullURLPattern = regexp.MustCompile(`^[a-z][-+\w]+:`)
|
v.fullURLPattern = regexp.MustCompile(`^[a-z][-+\w]+:`)
|
||||||
|
|
||||||
// emailRegex is definitely not perfect with edge cases,
|
// emailRegex is definitely not perfect with edge cases,
|
||||||
// it is still accepted by the CommonMark specification, as well as the HTML5 spec:
|
// it is still accepted by the CommonMark specification, as well as the HTML5 spec:
|
||||||
// http://spec.commonmark.org/0.28/#email-address
|
// http://spec.commonmark.org/0.28/#email-address
|
||||||
// https://html.spec.whatwg.org/multipage/input.html#e-mail-state-(type%3Demail)
|
// https://html.spec.whatwg.org/multipage/input.html#e-mail-state-(type%3Demail)
|
||||||
emailRegex = regexp.MustCompile("(?:\\s|^|\\(|\\[)([a-zA-Z0-9.!#$%&'*+\\/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\\.[a-zA-Z0-9]{2,}(?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)+)(?:\\s|$|\\)|\\]|;|,|\\?|!|\\.(\\s|$))")
|
v.emailRegex = regexp.MustCompile("(?:\\s|^|\\(|\\[)([a-zA-Z0-9.!#$%&'*+\\/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\\.[a-zA-Z0-9]{2,}(?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)+)(?:\\s|$|\\)|\\]|;|,|\\?|!|\\.(\\s|$))")
|
||||||
|
|
||||||
// blackfridayExtRegex is for blackfriday extensions create IDs like fn:user-content-footnote
|
// blackfridayExtRegex is for blackfriday extensions create IDs like fn:user-content-footnote
|
||||||
blackfridayExtRegex = regexp.MustCompile(`[^:]*:user-content-`)
|
v.blackfridayExtRegex = regexp.MustCompile(`[^:]*:user-content-`)
|
||||||
|
|
||||||
// emojiShortCodeRegex find emoji by alias like :smile:
|
// emojiShortCodeRegex find emoji by alias like :smile:
|
||||||
emojiShortCodeRegex = regexp.MustCompile(`:[-+\w]+:`)
|
v.emojiShortCodeRegex = regexp.MustCompile(`:[-+\w]+:`)
|
||||||
)
|
|
||||||
|
|
||||||
// CSS class for action keywords (e.g. "closes: #1")
|
// example: https://domain/org/repo/pulls/27#hash
|
||||||
const keywordClass = "issue-keyword"
|
v.issueFullPattern = regexp.MustCompile(`https?://(?:\S+/)[\w_.-]+/[\w_.-]+/(?:issues|pulls)/((?:\w{1,10}-)?[1-9][0-9]*)([\?|#](\S+)?)?\b`)
|
||||||
|
|
||||||
|
// example: https://domain/org/repo/pulls/27/files#hash
|
||||||
|
v.filesChangedFullPattern = regexp.MustCompile(`https?://(?:\S+/)[\w_.-]+/[\w_.-]+/pulls/((?:\w{1,10}-)?[1-9][0-9]*)/files([\?|#](\S+)?)?\b`)
|
||||||
|
|
||||||
|
v.tagCleaner = regexp.MustCompile(`<((?:/?\w+/\w+)|(?:/[\w ]+/)|(/?[hH][tT][mM][lL]\b)|(/?[hH][eE][aA][dD]\b))`)
|
||||||
|
v.nulCleaner = strings.NewReplacer("\000", "")
|
||||||
|
return v
|
||||||
|
})
|
||||||
|
|
||||||
// IsFullURLBytes reports whether link fits valid format.
|
// IsFullURLBytes reports whether link fits valid format.
|
||||||
func IsFullURLBytes(link []byte) bool {
|
func IsFullURLBytes(link []byte) bool {
|
||||||
return fullURLPattern.Match(link)
|
return globalVars().fullURLPattern.Match(link)
|
||||||
}
|
}
|
||||||
|
|
||||||
func IsFullURLString(link string) bool {
|
func IsFullURLString(link string) bool {
|
||||||
return fullURLPattern.MatchString(link)
|
return globalVars().fullURLPattern.MatchString(link)
|
||||||
}
|
}
|
||||||
|
|
||||||
func IsNonEmptyRelativePath(link string) bool {
|
func IsNonEmptyRelativePath(link string) bool {
|
||||||
return link != "" && !IsFullURLString(link) && link[0] != '/' && link[0] != '?' && link[0] != '#'
|
return link != "" && !IsFullURLString(link) && link[0] != '/' && link[0] != '?' && link[0] != '#'
|
||||||
}
|
}
|
||||||
|
|
||||||
// regexp for full links to issues/pulls
|
|
||||||
var issueFullPattern *regexp.Regexp
|
|
||||||
|
|
||||||
// Once for to prevent races
|
|
||||||
var issueFullPatternOnce sync.Once
|
|
||||||
|
|
||||||
// regexp for full links to hash comment in pull request files changed tab
|
|
||||||
var filesChangedFullPattern *regexp.Regexp
|
|
||||||
|
|
||||||
// Once for to prevent races
|
|
||||||
var filesChangedFullPatternOnce sync.Once
|
|
||||||
|
|
||||||
func getIssueFullPattern() *regexp.Regexp {
|
|
||||||
issueFullPatternOnce.Do(func() {
|
|
||||||
// example: https://domain/org/repo/pulls/27#hash
|
|
||||||
issueFullPattern = regexp.MustCompile(regexp.QuoteMeta(setting.AppURL) +
|
|
||||||
`[\w_.-]+/[\w_.-]+/(?:issues|pulls)/((?:\w{1,10}-)?[1-9][0-9]*)([\?|#](\S+)?)?\b`)
|
|
||||||
})
|
|
||||||
return issueFullPattern
|
|
||||||
}
|
|
||||||
|
|
||||||
func getFilesChangedFullPattern() *regexp.Regexp {
|
|
||||||
filesChangedFullPatternOnce.Do(func() {
|
|
||||||
// example: https://domain/org/repo/pulls/27/files#hash
|
|
||||||
filesChangedFullPattern = regexp.MustCompile(regexp.QuoteMeta(setting.AppURL) +
|
|
||||||
`[\w_.-]+/[\w_.-]+/pulls/((?:\w{1,10}-)?[1-9][0-9]*)/files([\?|#](\S+)?)?\b`)
|
|
||||||
})
|
|
||||||
return filesChangedFullPattern
|
|
||||||
}
|
|
||||||
|
|
||||||
// CustomLinkURLSchemes allows for additional schemes to be detected when parsing links within text
|
// CustomLinkURLSchemes allows for additional schemes to be detected when parsing links within text
|
||||||
func CustomLinkURLSchemes(schemes []string) {
|
func CustomLinkURLSchemes(schemes []string) {
|
||||||
schemes = append(schemes, "http", "https")
|
schemes = append(schemes, "http", "https")
|
||||||
|
@ -197,13 +194,6 @@ func RenderCommitMessage(
|
||||||
content string,
|
content string,
|
||||||
) (string, error) {
|
) (string, error) {
|
||||||
procs := commitMessageProcessors
|
procs := commitMessageProcessors
|
||||||
if ctx.DefaultLink != "" {
|
|
||||||
// we don't have to fear data races, because being
|
|
||||||
// commitMessageProcessors of fixed len and cap, every time we append
|
|
||||||
// something to it the slice is realloc+copied, so append always
|
|
||||||
// generates the slice ex-novo.
|
|
||||||
procs = append(procs, genDefaultLinkProcessor(ctx.DefaultLink))
|
|
||||||
}
|
|
||||||
return renderProcessString(ctx, procs, content)
|
return renderProcessString(ctx, procs, content)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -231,16 +221,17 @@ var emojiProcessors = []processor{
|
||||||
// which changes every text node into a link to the passed default link.
|
// which changes every text node into a link to the passed default link.
|
||||||
func RenderCommitMessageSubject(
|
func RenderCommitMessageSubject(
|
||||||
ctx *RenderContext,
|
ctx *RenderContext,
|
||||||
content string,
|
defaultLink, content string,
|
||||||
) (string, error) {
|
) (string, error) {
|
||||||
procs := commitMessageSubjectProcessors
|
procs := slices.Clone(commitMessageSubjectProcessors)
|
||||||
if ctx.DefaultLink != "" {
|
procs = append(procs, func(ctx *RenderContext, node *html.Node) {
|
||||||
// we don't have to fear data races, because being
|
ch := &html.Node{Parent: node, Type: html.TextNode, Data: node.Data}
|
||||||
// commitMessageSubjectProcessors of fixed len and cap, every time we
|
node.Type = html.ElementNode
|
||||||
// append something to it the slice is realloc+copied, so append always
|
node.Data = "a"
|
||||||
// generates the slice ex-novo.
|
node.DataAtom = atom.A
|
||||||
procs = append(procs, genDefaultLinkProcessor(ctx.DefaultLink))
|
node.Attr = []html.Attribute{{Key: "href", Val: defaultLink}, {Key: "class", Val: "muted"}}
|
||||||
}
|
node.FirstChild, node.LastChild = ch, ch
|
||||||
|
})
|
||||||
return renderProcessString(ctx, procs, content)
|
return renderProcessString(ctx, procs, content)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -249,10 +240,8 @@ func RenderIssueTitle(
|
||||||
ctx *RenderContext,
|
ctx *RenderContext,
|
||||||
title string,
|
title string,
|
||||||
) (string, error) {
|
) (string, error) {
|
||||||
|
// do not render other issue/commit links in an issue's title - which in most cases is already a link.
|
||||||
return renderProcessString(ctx, []processor{
|
return renderProcessString(ctx, []processor{
|
||||||
issueIndexPatternProcessor,
|
|
||||||
commitCrossReferencePatternProcessor,
|
|
||||||
hashCurrentPatternProcessor,
|
|
||||||
emojiShortCodeProcessor,
|
emojiShortCodeProcessor,
|
||||||
emojiProcessor,
|
emojiProcessor,
|
||||||
}, title)
|
}, title)
|
||||||
|
@ -288,11 +277,6 @@ func RenderEmoji(
|
||||||
return renderProcessString(ctx, emojiProcessors, content)
|
return renderProcessString(ctx, emojiProcessors, content)
|
||||||
}
|
}
|
||||||
|
|
||||||
var (
|
|
||||||
tagCleaner = regexp.MustCompile(`<((?:/?\w+/\w+)|(?:/[\w ]+/)|(/?[hH][tT][mM][lL]\b)|(/?[hH][eE][aA][dD]\b))`)
|
|
||||||
nulCleaner = strings.NewReplacer("\000", "")
|
|
||||||
)
|
|
||||||
|
|
||||||
func postProcess(ctx *RenderContext, procs []processor, input io.Reader, output io.Writer) error {
|
func postProcess(ctx *RenderContext, procs []processor, input io.Reader, output io.Writer) error {
|
||||||
defer ctx.Cancel()
|
defer ctx.Cancel()
|
||||||
// FIXME: don't read all content to memory
|
// FIXME: don't read all content to memory
|
||||||
|
@ -306,7 +290,7 @@ func postProcess(ctx *RenderContext, procs []processor, input io.Reader, output
|
||||||
// prepend "<html><body>"
|
// prepend "<html><body>"
|
||||||
strings.NewReader("<html><body>"),
|
strings.NewReader("<html><body>"),
|
||||||
// Strip out nuls - they're always invalid
|
// Strip out nuls - they're always invalid
|
||||||
bytes.NewReader(tagCleaner.ReplaceAll([]byte(nulCleaner.Replace(string(rawHTML))), []byte("<$1"))),
|
bytes.NewReader(globalVars().tagCleaner.ReplaceAll([]byte(globalVars().nulCleaner.Replace(string(rawHTML))), []byte("<$1"))),
|
||||||
// close the tags
|
// close the tags
|
||||||
strings.NewReader("</body></html>"),
|
strings.NewReader("</body></html>"),
|
||||||
))
|
))
|
||||||
|
@ -353,7 +337,7 @@ func visitNode(ctx *RenderContext, procs []processor, node *html.Node) *html.Nod
|
||||||
// Add user-content- to IDs and "#" links if they don't already have them
|
// Add user-content- to IDs and "#" links if they don't already have them
|
||||||
for idx, attr := range node.Attr {
|
for idx, attr := range node.Attr {
|
||||||
val := strings.TrimPrefix(attr.Val, "#")
|
val := strings.TrimPrefix(attr.Val, "#")
|
||||||
notHasPrefix := !(strings.HasPrefix(val, "user-content-") || blackfridayExtRegex.MatchString(val))
|
notHasPrefix := !(strings.HasPrefix(val, "user-content-") || globalVars().blackfridayExtRegex.MatchString(val))
|
||||||
|
|
||||||
if attr.Key == "id" && notHasPrefix {
|
if attr.Key == "id" && notHasPrefix {
|
||||||
node.Attr[idx].Val = "user-content-" + attr.Val
|
node.Attr[idx].Val = "user-content-" + attr.Val
|
||||||
|
@ -442,12 +426,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))
|
||||||
|
|
|
@ -54,7 +54,7 @@ func createCodeLink(href, content, class string) *html.Node {
|
||||||
}
|
}
|
||||||
|
|
||||||
func anyHashPatternExtract(s string) (ret anyHashPatternResult, ok bool) {
|
func anyHashPatternExtract(s string) (ret anyHashPatternResult, ok bool) {
|
||||||
m := anyHashPattern.FindStringSubmatchIndex(s)
|
m := globalVars().anyHashPattern.FindStringSubmatchIndex(s)
|
||||||
if m == nil {
|
if m == nil {
|
||||||
return ret, false
|
return ret, false
|
||||||
}
|
}
|
||||||
|
@ -120,7 +120,7 @@ func comparePatternProcessor(ctx *RenderContext, node *html.Node) {
|
||||||
node = node.NextSibling
|
node = node.NextSibling
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
m := comparePattern.FindStringSubmatchIndex(node.Data)
|
m := globalVars().comparePattern.FindStringSubmatchIndex(node.Data)
|
||||||
if m == nil || slices.Contains(m[:8], -1) { // ensure that every group (m[0]...m[7]) has a match
|
if m == nil || slices.Contains(m[:8], -1) { // ensure that every group (m[0]...m[7]) has a match
|
||||||
node = node.NextSibling
|
node = node.NextSibling
|
||||||
continue
|
continue
|
||||||
|
@ -173,7 +173,7 @@ func hashCurrentPatternProcessor(ctx *RenderContext, node *html.Node) {
|
||||||
ctx.ShaExistCache = make(map[string]bool)
|
ctx.ShaExistCache = make(map[string]bool)
|
||||||
}
|
}
|
||||||
for node != nil && node != next && start < len(node.Data) {
|
for node != nil && node != next && start < len(node.Data) {
|
||||||
m := hashCurrentPattern.FindStringSubmatchIndex(node.Data[start:])
|
m := globalVars().hashCurrentPattern.FindStringSubmatchIndex(node.Data[start:])
|
||||||
if m == nil {
|
if m == nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,7 +9,7 @@ import "golang.org/x/net/html"
|
||||||
func emailAddressProcessor(ctx *RenderContext, node *html.Node) {
|
func emailAddressProcessor(ctx *RenderContext, node *html.Node) {
|
||||||
next := node.NextSibling
|
next := node.NextSibling
|
||||||
for node != nil && node != next {
|
for node != nil && node != next {
|
||||||
m := emailRegex.FindStringSubmatchIndex(node.Data)
|
m := globalVars().emailRegex.FindStringSubmatchIndex(node.Data)
|
||||||
if m == nil {
|
if m == nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
|
@ -62,7 +62,7 @@ func emojiShortCodeProcessor(ctx *RenderContext, node *html.Node) {
|
||||||
start := 0
|
start := 0
|
||||||
next := node.NextSibling
|
next := node.NextSibling
|
||||||
for node != nil && node != next && start < len(node.Data) {
|
for node != nil && node != next && start < len(node.Data) {
|
||||||
m := emojiShortCodeRegex.FindStringSubmatchIndex(node.Data[start:])
|
m := globalVars().emojiShortCodeRegex.FindStringSubmatchIndex(node.Data[start:])
|
||||||
if m == nil {
|
if m == nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
|
@ -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"
|
||||||
|
@ -39,17 +40,19 @@ func link(href, class, contents string) string {
|
||||||
}
|
}
|
||||||
|
|
||||||
var numericMetas = map[string]string{
|
var numericMetas = map[string]string{
|
||||||
"format": "https://someurl.com/{user}/{repo}/{index}",
|
"format": "https://someurl.com/{user}/{repo}/{index}",
|
||||||
"user": "someUser",
|
"user": "someUser",
|
||||||
"repo": "someRepo",
|
"repo": "someRepo",
|
||||||
"style": IssueNameStyleNumeric,
|
"style": IssueNameStyleNumeric,
|
||||||
|
"markupAllowShortIssuePattern": "true",
|
||||||
}
|
}
|
||||||
|
|
||||||
var alphanumericMetas = map[string]string{
|
var alphanumericMetas = map[string]string{
|
||||||
"format": "https://someurl.com/{user}/{repo}/{index}",
|
"format": "https://someurl.com/{user}/{repo}/{index}",
|
||||||
"user": "someUser",
|
"user": "someUser",
|
||||||
"repo": "someRepo",
|
"repo": "someRepo",
|
||||||
"style": IssueNameStyleAlphanumeric,
|
"style": IssueNameStyleAlphanumeric,
|
||||||
|
"markupAllowShortIssuePattern": "true",
|
||||||
}
|
}
|
||||||
|
|
||||||
var regexpMetas = map[string]string{
|
var regexpMetas = map[string]string{
|
||||||
|
@ -61,8 +64,15 @@ var regexpMetas = map[string]string{
|
||||||
|
|
||||||
// these values should match the TestOrgRepo const above
|
// these values should match the TestOrgRepo const above
|
||||||
var localMetas = map[string]string{
|
var localMetas = map[string]string{
|
||||||
"user": "test-owner",
|
"user": "test-owner",
|
||||||
"repo": "test-repo",
|
"repo": "test-repo",
|
||||||
|
"markupAllowShortIssuePattern": "true",
|
||||||
|
}
|
||||||
|
|
||||||
|
var localWikiMetas = map[string]string{
|
||||||
|
"user": "test-owner",
|
||||||
|
"repo": "test-repo",
|
||||||
|
"markupContentMode": "wiki",
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestRender_IssueIndexPattern(t *testing.T) {
|
func TestRender_IssueIndexPattern(t *testing.T) {
|
||||||
|
@ -259,14 +269,13 @@ func TestRender_IssueIndexPattern5(t *testing.T) {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestRender_IssueIndexPattern_Document(t *testing.T) {
|
func TestRender_IssueIndexPattern_NoShortPattern(t *testing.T) {
|
||||||
setting.AppURL = TestAppURL
|
setting.AppURL = TestAppURL
|
||||||
metas := map[string]string{
|
metas := map[string]string{
|
||||||
"format": "https://someurl.com/{user}/{repo}/{index}",
|
"format": "https://someurl.com/{user}/{repo}/{index}",
|
||||||
"user": "someUser",
|
"user": "someUser",
|
||||||
"repo": "someRepo",
|
"repo": "someRepo",
|
||||||
"style": IssueNameStyleNumeric,
|
"style": IssueNameStyleNumeric,
|
||||||
"mode": "document",
|
|
||||||
}
|
}
|
||||||
|
|
||||||
testRenderIssueIndexPattern(t, "#1", "#1", &RenderContext{
|
testRenderIssueIndexPattern(t, "#1", "#1", &RenderContext{
|
||||||
|
@ -283,6 +292,22 @@ func TestRender_IssueIndexPattern_Document(t *testing.T) {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestRender_RenderIssueTitle(t *testing.T) {
|
||||||
|
setting.AppURL = TestAppURL
|
||||||
|
metas := map[string]string{
|
||||||
|
"format": "https://someurl.com/{user}/{repo}/{index}",
|
||||||
|
"user": "someUser",
|
||||||
|
"repo": "someRepo",
|
||||||
|
"style": IssueNameStyleNumeric,
|
||||||
|
}
|
||||||
|
actual, err := RenderIssueTitle(&RenderContext{
|
||||||
|
Ctx: git.DefaultContext,
|
||||||
|
Metas: metas,
|
||||||
|
}, "#1")
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, "#1", actual)
|
||||||
|
}
|
||||||
|
|
||||||
func testRenderIssueIndexPattern(t *testing.T, input, expected string, ctx *RenderContext) {
|
func testRenderIssueIndexPattern(t *testing.T, input, expected string, ctx *RenderContext) {
|
||||||
ctx.Links.AbsolutePrefix = true
|
ctx.Links.AbsolutePrefix = true
|
||||||
if ctx.Links.Base == "" {
|
if ctx.Links.Base == "" {
|
||||||
|
@ -316,8 +341,7 @@ func TestRender_AutoLink(t *testing.T) {
|
||||||
Links: Links{
|
Links: Links{
|
||||||
Base: TestRepoURL,
|
Base: TestRepoURL,
|
||||||
},
|
},
|
||||||
Metas: localMetas,
|
Metas: localWikiMetas,
|
||||||
IsWiki: true,
|
|
||||||
}, 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 +364,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 +375,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")
|
||||||
|
@ -391,10 +413,10 @@ func TestRegExp_sha1CurrentPattern(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, testCase := range trueTestCases {
|
for _, testCase := range trueTestCases {
|
||||||
assert.True(t, hashCurrentPattern.MatchString(testCase))
|
assert.True(t, globalVars().hashCurrentPattern.MatchString(testCase))
|
||||||
}
|
}
|
||||||
for _, testCase := range falseTestCases {
|
for _, testCase := range falseTestCases {
|
||||||
assert.False(t, hashCurrentPattern.MatchString(testCase))
|
assert.False(t, globalVars().hashCurrentPattern.MatchString(testCase))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -474,9 +496,9 @@ func TestRegExp_shortLinkPattern(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, testCase := range trueTestCases {
|
for _, testCase := range trueTestCases {
|
||||||
assert.True(t, shortLinkPattern.MatchString(testCase))
|
assert.True(t, globalVars().shortLinkPattern.MatchString(testCase))
|
||||||
}
|
}
|
||||||
for _, testCase := range falseTestCases {
|
for _, testCase := range falseTestCases {
|
||||||
assert.False(t, shortLinkPattern.MatchString(testCase))
|
assert.False(t, globalVars().shortLinkPattern.MatchString(testCase))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,6 +7,7 @@ import (
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"code.gitea.io/gitea/modules/base"
|
"code.gitea.io/gitea/modules/base"
|
||||||
|
"code.gitea.io/gitea/modules/httplib"
|
||||||
"code.gitea.io/gitea/modules/log"
|
"code.gitea.io/gitea/modules/log"
|
||||||
"code.gitea.io/gitea/modules/references"
|
"code.gitea.io/gitea/modules/references"
|
||||||
"code.gitea.io/gitea/modules/regexplru"
|
"code.gitea.io/gitea/modules/regexplru"
|
||||||
|
@ -23,18 +24,21 @@ func fullIssuePatternProcessor(ctx *RenderContext, node *html.Node) {
|
||||||
}
|
}
|
||||||
next := node.NextSibling
|
next := node.NextSibling
|
||||||
for node != nil && node != next {
|
for node != nil && node != next {
|
||||||
m := getIssueFullPattern().FindStringSubmatchIndex(node.Data)
|
m := globalVars().issueFullPattern.FindStringSubmatchIndex(node.Data)
|
||||||
if m == nil {
|
if m == nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
mDiffView := getFilesChangedFullPattern().FindStringSubmatchIndex(node.Data)
|
mDiffView := globalVars().filesChangedFullPattern.FindStringSubmatchIndex(node.Data)
|
||||||
// leave it as it is if the link is from "Files Changed" tab in PR Diff View https://domain/org/repo/pulls/27/files
|
// leave it as it is if the link is from "Files Changed" tab in PR Diff View https://domain/org/repo/pulls/27/files
|
||||||
if mDiffView != nil {
|
if mDiffView != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
link := node.Data[m[0]:m[1]]
|
link := node.Data[m[0]:m[1]]
|
||||||
|
if !httplib.IsCurrentGiteaSiteURL(ctx.Ctx, link) {
|
||||||
|
return
|
||||||
|
}
|
||||||
text := "#" + node.Data[m[2]:m[3]]
|
text := "#" + node.Data[m[2]:m[3]]
|
||||||
// if m[4] and m[5] is not -1, then link is to a comment
|
// if m[4] and m[5] is not -1, then link is to a comment
|
||||||
// indicate that in the text by appending (comment)
|
// indicate that in the text by appending (comment)
|
||||||
|
@ -67,9 +71,10 @@ 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: do not parse "#123", only parse "owner/repo#123"
|
||||||
// The "mode" approach should be refactored to some other more clear&reliable way.
|
// if there is no repo in the context, then the "#123" format can't be parsed
|
||||||
crossLinkOnly := ctx.Metas["mode"] == "document" && !ctx.IsWiki
|
// old logic: crossLinkOnly := ctx.Metas["mode"] == "document" && !ctx.IsWiki
|
||||||
|
crossLinkOnly := ctx.Metas["markupAllowShortIssuePattern"] != "true"
|
||||||
|
|
||||||
var (
|
var (
|
||||||
found bool
|
found bool
|
||||||
|
|
|
@ -20,9 +20,9 @@ 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.IsMarkupContentWiki() {
|
||||||
// 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()
|
||||||
} else if ctx.Links.BranchPath != "" || ctx.Links.TreePath != "" {
|
} else if ctx.Links.BranchPath != "" || ctx.Links.TreePath != "" {
|
||||||
// if there is no BranchPath, then the link will be something like "/owner/repo/src/{the-file-path}"
|
// if there is no BranchPath, then the link will be something like "/owner/repo/src/{the-file-path}"
|
||||||
|
@ -40,7 +40,7 @@ func ResolveLink(ctx *RenderContext, link, userContentAnchorPrefix string) (resu
|
||||||
func shortLinkProcessor(ctx *RenderContext, node *html.Node) {
|
func shortLinkProcessor(ctx *RenderContext, node *html.Node) {
|
||||||
next := node.NextSibling
|
next := node.NextSibling
|
||||||
for node != nil && node != next {
|
for node != nil && node != next {
|
||||||
m := shortLinkPattern.FindStringSubmatchIndex(node.Data)
|
m := globalVars().shortLinkPattern.FindStringSubmatchIndex(node.Data)
|
||||||
if m == nil {
|
if m == nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -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.IsMarkupContentWiki()), link)
|
||||||
}
|
}
|
||||||
title := props["title"]
|
title := props["title"]
|
||||||
if title == "" {
|
if title == "" {
|
||||||
|
@ -200,25 +200,6 @@ func linkProcessor(ctx *RenderContext, node *html.Node) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func genDefaultLinkProcessor(defaultLink string) processor {
|
|
||||||
return func(ctx *RenderContext, node *html.Node) {
|
|
||||||
ch := &html.Node{
|
|
||||||
Parent: node,
|
|
||||||
Type: html.TextNode,
|
|
||||||
Data: node.Data,
|
|
||||||
}
|
|
||||||
|
|
||||||
node.Type = html.ElementNode
|
|
||||||
node.Data = "a"
|
|
||||||
node.DataAtom = atom.A
|
|
||||||
node.Attr = []html.Attribute{
|
|
||||||
{Key: "href", Val: defaultLink},
|
|
||||||
{Key: "class", Val: "default-link muted"},
|
|
||||||
}
|
|
||||||
node.FirstChild, node.LastChild = ch, ch
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// descriptionLinkProcessor creates links for DescriptionHTML
|
// descriptionLinkProcessor creates links for DescriptionHTML
|
||||||
func descriptionLinkProcessor(ctx *RenderContext, node *html.Node) {
|
func descriptionLinkProcessor(ctx *RenderContext, node *html.Node) {
|
||||||
next := node.NextSibling
|
next := node.NextSibling
|
||||||
|
|
|
@ -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.IsMarkupContentWiki()), 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.IsMarkupContentWiki()), 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"
|
||||||
|
@ -26,6 +27,11 @@ var (
|
||||||
"user": testRepoOwnerName,
|
"user": testRepoOwnerName,
|
||||||
"repo": testRepoName,
|
"repo": testRepoName,
|
||||||
}
|
}
|
||||||
|
localWikiMetas = map[string]string{
|
||||||
|
"user": testRepoOwnerName,
|
||||||
|
"repo": testRepoName,
|
||||||
|
"markupContentMode": "wiki",
|
||||||
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
type mockRepo struct {
|
type mockRepo struct {
|
||||||
|
@ -104,7 +110,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 +122,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 +152,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 +162,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 +263,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 +273,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 +302,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 +418,7 @@ func TestRender_ShortLinks(t *testing.T) {
|
||||||
Links: markup.Links{
|
Links: markup.Links{
|
||||||
Base: markup.TestRepoURL,
|
Base: markup.TestRepoURL,
|
||||||
},
|
},
|
||||||
Metas: localMetas,
|
Metas: localWikiMetas,
|
||||||
IsWiki: true,
|
|
||||||
}, 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 +530,9 @@ 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: util.Iif(isWiki, localWikiMetas, localMetas),
|
||||||
IsWiki: isWiki,
|
|
||||||
}, input)
|
}, input)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
return strings.TrimSpace(string(buffer))
|
return strings.TrimSpace(string(buffer))
|
||||||
|
@ -604,12 +602,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 +612,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,9 +72,15 @@ 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.
|
||||||
|
markdownLineBreakStyle := ctx.Metas["markdownLineBreakStyle"]
|
||||||
|
if markup.RenderBehaviorForTesting.ForceHardLineBreak {
|
||||||
|
v.SetHardLineBreak(true)
|
||||||
|
} else if markdownLineBreakStyle == "comment" {
|
||||||
v.SetHardLineBreak(setting.Markdown.EnableHardLineBreakInComments)
|
v.SetHardLineBreak(setting.Markdown.EnableHardLineBreakInComments)
|
||||||
} else {
|
} else if markdownLineBreakStyle == "document" {
|
||||||
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"
|
||||||
|
@ -36,6 +37,12 @@ var localMetas = map[string]string{
|
||||||
"repo": testRepoName,
|
"repo": testRepoName,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var localWikiMetas = map[string]string{
|
||||||
|
"user": testRepoOwnerName,
|
||||||
|
"repo": testRepoName,
|
||||||
|
"markupContentMode": "wiki",
|
||||||
|
}
|
||||||
|
|
||||||
type mockRepo struct {
|
type mockRepo struct {
|
||||||
OwnerName string
|
OwnerName string
|
||||||
RepoName string
|
RepoName string
|
||||||
|
@ -74,7 +81,7 @@ func TestRender_StandardLinks(t *testing.T) {
|
||||||
Links: markup.Links{
|
Links: markup.Links{
|
||||||
Base: FullURL,
|
Base: FullURL,
|
||||||
},
|
},
|
||||||
IsWiki: true,
|
Metas: localWikiMetas,
|
||||||
}, 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 +303,21 @@ 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: localWikiMetas,
|
||||||
IsWiki: true,
|
|
||||||
}, 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 +339,18 @@ func TestTotal_RenderWiki(t *testing.T) {
|
||||||
Links: markup.Links{
|
Links: markup.Links{
|
||||||
Base: FullURL,
|
Base: FullURL,
|
||||||
},
|
},
|
||||||
IsWiki: true,
|
Metas: localWikiMetas,
|
||||||
}, 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 +362,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 +431,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)
|
||||||
|
@ -658,9 +662,9 @@ mail@domain.com
|
||||||
<a href="https://example.com/image.jpg" target="_blank" rel="nofollow noopener"><img src="https://example.com/image.jpg" alt="remote image"/></a><br/>
|
<a href="https://example.com/image.jpg" target="_blank" rel="nofollow noopener"><img src="https://example.com/image.jpg" alt="remote image"/></a><br/>
|
||||||
<a href="/image.jpg" rel="nofollow"><img src="/image.jpg" title="local image" alt="local image"/></a><br/>
|
<a href="/image.jpg" rel="nofollow"><img src="/image.jpg" title="local image" alt="local image"/></a><br/>
|
||||||
<a href="https://example.com/image.jpg" rel="nofollow"><img src="https://example.com/image.jpg" title="remote link" alt="remote link"/></a><br/>
|
<a href="https://example.com/image.jpg" rel="nofollow"><img src="https://example.com/image.jpg" title="remote link" alt="remote link"/></a><br/>
|
||||||
<a href="https://example.com/user/repo/compare/88fc37a3c0a4dda553bdcfc80c178a58247f42fb...12fc37a3c0a4dda553bdcfc80c178a58247f42fb#hash" rel="nofollow">https://example.com/user/repo/compare/88fc37a3c0a4dda553bdcfc80c178a58247f42fb...12fc37a3c0a4dda553bdcfc80c178a58247f42fb#hash</a><br/>
|
<a href="https://example.com/user/repo/compare/88fc37a3c0a4dda553bdcfc80c178a58247f42fb...12fc37a3c0a4dda553bdcfc80c178a58247f42fb#hash" rel="nofollow"><code>88fc37a3c0...12fc37a3c0 (hash)</code></a><br/>
|
||||||
com 88fc37a3c0a4dda553bdcfc80c178a58247f42fb...12fc37a3c0a4dda553bdcfc80c178a58247f42fb pare<br/>
|
com 88fc37a3c0a4dda553bdcfc80c178a58247f42fb...12fc37a3c0a4dda553bdcfc80c178a58247f42fb pare<br/>
|
||||||
<a href="https://example.com/user/repo/commit/88fc37a3c0a4dda553bdcfc80c178a58247f42fb" rel="nofollow">https://example.com/user/repo/commit/88fc37a3c0a4dda553bdcfc80c178a58247f42fb</a><br/>
|
<a href="https://example.com/user/repo/commit/88fc37a3c0a4dda553bdcfc80c178a58247f42fb" rel="nofollow"><code>88fc37a3c0</code></a><br/>
|
||||||
com 88fc37a3c0a4dda553bdcfc80c178a58247f42fb mit<br/>
|
com 88fc37a3c0a4dda553bdcfc80c178a58247f42fb mit<br/>
|
||||||
<span class="emoji" aria-label="thumbs up">👍</span><br/>
|
<span class="emoji" aria-label="thumbs up">👍</span><br/>
|
||||||
<a href="mailto:mail@domain.com" rel="nofollow">mail@domain.com</a><br/>
|
<a href="mailto:mail@domain.com" rel="nofollow">mail@domain.com</a><br/>
|
||||||
|
@ -685,9 +689,9 @@ space</p>
|
||||||
<a href="https://example.com/image.jpg" target="_blank" rel="nofollow noopener"><img src="https://example.com/image.jpg" alt="remote image"/></a><br/>
|
<a href="https://example.com/image.jpg" target="_blank" rel="nofollow noopener"><img src="https://example.com/image.jpg" alt="remote image"/></a><br/>
|
||||||
<a href="/wiki/raw/image.jpg" rel="nofollow"><img src="/wiki/raw/image.jpg" title="local image" alt="local image"/></a><br/>
|
<a href="/wiki/raw/image.jpg" rel="nofollow"><img src="/wiki/raw/image.jpg" title="local image" alt="local image"/></a><br/>
|
||||||
<a href="https://example.com/image.jpg" rel="nofollow"><img src="https://example.com/image.jpg" title="remote link" alt="remote link"/></a><br/>
|
<a href="https://example.com/image.jpg" rel="nofollow"><img src="https://example.com/image.jpg" title="remote link" alt="remote link"/></a><br/>
|
||||||
<a href="https://example.com/user/repo/compare/88fc37a3c0a4dda553bdcfc80c178a58247f42fb...12fc37a3c0a4dda553bdcfc80c178a58247f42fb#hash" rel="nofollow">https://example.com/user/repo/compare/88fc37a3c0a4dda553bdcfc80c178a58247f42fb...12fc37a3c0a4dda553bdcfc80c178a58247f42fb#hash</a><br/>
|
<a href="https://example.com/user/repo/compare/88fc37a3c0a4dda553bdcfc80c178a58247f42fb...12fc37a3c0a4dda553bdcfc80c178a58247f42fb#hash" rel="nofollow"><code>88fc37a3c0...12fc37a3c0 (hash)</code></a><br/>
|
||||||
com 88fc37a3c0a4dda553bdcfc80c178a58247f42fb...12fc37a3c0a4dda553bdcfc80c178a58247f42fb pare<br/>
|
com 88fc37a3c0a4dda553bdcfc80c178a58247f42fb...12fc37a3c0a4dda553bdcfc80c178a58247f42fb pare<br/>
|
||||||
<a href="https://example.com/user/repo/commit/88fc37a3c0a4dda553bdcfc80c178a58247f42fb" rel="nofollow">https://example.com/user/repo/commit/88fc37a3c0a4dda553bdcfc80c178a58247f42fb</a><br/>
|
<a href="https://example.com/user/repo/commit/88fc37a3c0a4dda553bdcfc80c178a58247f42fb" rel="nofollow"><code>88fc37a3c0</code></a><br/>
|
||||||
com 88fc37a3c0a4dda553bdcfc80c178a58247f42fb mit<br/>
|
com 88fc37a3c0a4dda553bdcfc80c178a58247f42fb mit<br/>
|
||||||
<span class="emoji" aria-label="thumbs up">👍</span><br/>
|
<span class="emoji" aria-label="thumbs up">👍</span><br/>
|
||||||
<a href="mailto:mail@domain.com" rel="nofollow">mail@domain.com</a><br/>
|
<a href="mailto:mail@domain.com" rel="nofollow">mail@domain.com</a><br/>
|
||||||
|
@ -714,9 +718,9 @@ space</p>
|
||||||
<a href="https://example.com/image.jpg" target="_blank" rel="nofollow noopener"><img src="https://example.com/image.jpg" alt="remote image"/></a><br/>
|
<a href="https://example.com/image.jpg" target="_blank" rel="nofollow noopener"><img src="https://example.com/image.jpg" alt="remote image"/></a><br/>
|
||||||
<a href="https://gitea.io/image.jpg" rel="nofollow"><img src="https://gitea.io/image.jpg" title="local image" alt="local image"/></a><br/>
|
<a href="https://gitea.io/image.jpg" rel="nofollow"><img src="https://gitea.io/image.jpg" title="local image" alt="local image"/></a><br/>
|
||||||
<a href="https://example.com/image.jpg" rel="nofollow"><img src="https://example.com/image.jpg" title="remote link" alt="remote link"/></a><br/>
|
<a href="https://example.com/image.jpg" rel="nofollow"><img src="https://example.com/image.jpg" title="remote link" alt="remote link"/></a><br/>
|
||||||
<a href="https://example.com/user/repo/compare/88fc37a3c0a4dda553bdcfc80c178a58247f42fb...12fc37a3c0a4dda553bdcfc80c178a58247f42fb#hash" rel="nofollow">https://example.com/user/repo/compare/88fc37a3c0a4dda553bdcfc80c178a58247f42fb...12fc37a3c0a4dda553bdcfc80c178a58247f42fb#hash</a><br/>
|
<a href="https://example.com/user/repo/compare/88fc37a3c0a4dda553bdcfc80c178a58247f42fb...12fc37a3c0a4dda553bdcfc80c178a58247f42fb#hash" rel="nofollow"><code>88fc37a3c0...12fc37a3c0 (hash)</code></a><br/>
|
||||||
com 88fc37a3c0a4dda553bdcfc80c178a58247f42fb...12fc37a3c0a4dda553bdcfc80c178a58247f42fb pare<br/>
|
com 88fc37a3c0a4dda553bdcfc80c178a58247f42fb...12fc37a3c0a4dda553bdcfc80c178a58247f42fb pare<br/>
|
||||||
<a href="https://example.com/user/repo/commit/88fc37a3c0a4dda553bdcfc80c178a58247f42fb" rel="nofollow">https://example.com/user/repo/commit/88fc37a3c0a4dda553bdcfc80c178a58247f42fb</a><br/>
|
<a href="https://example.com/user/repo/commit/88fc37a3c0a4dda553bdcfc80c178a58247f42fb" rel="nofollow"><code>88fc37a3c0</code></a><br/>
|
||||||
com 88fc37a3c0a4dda553bdcfc80c178a58247f42fb mit<br/>
|
com 88fc37a3c0a4dda553bdcfc80c178a58247f42fb mit<br/>
|
||||||
<span class="emoji" aria-label="thumbs up">👍</span><br/>
|
<span class="emoji" aria-label="thumbs up">👍</span><br/>
|
||||||
<a href="mailto:mail@domain.com" rel="nofollow">mail@domain.com</a><br/>
|
<a href="mailto:mail@domain.com" rel="nofollow">mail@domain.com</a><br/>
|
||||||
|
@ -743,9 +747,9 @@ space</p>
|
||||||
<a href="https://example.com/image.jpg" target="_blank" rel="nofollow noopener"><img src="https://example.com/image.jpg" alt="remote image"/></a><br/>
|
<a href="https://example.com/image.jpg" target="_blank" rel="nofollow noopener"><img src="https://example.com/image.jpg" alt="remote image"/></a><br/>
|
||||||
<a href="https://gitea.io/wiki/raw/image.jpg" rel="nofollow"><img src="https://gitea.io/wiki/raw/image.jpg" title="local image" alt="local image"/></a><br/>
|
<a href="https://gitea.io/wiki/raw/image.jpg" rel="nofollow"><img src="https://gitea.io/wiki/raw/image.jpg" title="local image" alt="local image"/></a><br/>
|
||||||
<a href="https://example.com/image.jpg" rel="nofollow"><img src="https://example.com/image.jpg" title="remote link" alt="remote link"/></a><br/>
|
<a href="https://example.com/image.jpg" rel="nofollow"><img src="https://example.com/image.jpg" title="remote link" alt="remote link"/></a><br/>
|
||||||
<a href="https://example.com/user/repo/compare/88fc37a3c0a4dda553bdcfc80c178a58247f42fb...12fc37a3c0a4dda553bdcfc80c178a58247f42fb#hash" rel="nofollow">https://example.com/user/repo/compare/88fc37a3c0a4dda553bdcfc80c178a58247f42fb...12fc37a3c0a4dda553bdcfc80c178a58247f42fb#hash</a><br/>
|
<a href="https://example.com/user/repo/compare/88fc37a3c0a4dda553bdcfc80c178a58247f42fb...12fc37a3c0a4dda553bdcfc80c178a58247f42fb#hash" rel="nofollow"><code>88fc37a3c0...12fc37a3c0 (hash)</code></a><br/>
|
||||||
com 88fc37a3c0a4dda553bdcfc80c178a58247f42fb...12fc37a3c0a4dda553bdcfc80c178a58247f42fb pare<br/>
|
com 88fc37a3c0a4dda553bdcfc80c178a58247f42fb...12fc37a3c0a4dda553bdcfc80c178a58247f42fb pare<br/>
|
||||||
<a href="https://example.com/user/repo/commit/88fc37a3c0a4dda553bdcfc80c178a58247f42fb" rel="nofollow">https://example.com/user/repo/commit/88fc37a3c0a4dda553bdcfc80c178a58247f42fb</a><br/>
|
<a href="https://example.com/user/repo/commit/88fc37a3c0a4dda553bdcfc80c178a58247f42fb" rel="nofollow"><code>88fc37a3c0</code></a><br/>
|
||||||
com 88fc37a3c0a4dda553bdcfc80c178a58247f42fb mit<br/>
|
com 88fc37a3c0a4dda553bdcfc80c178a58247f42fb mit<br/>
|
||||||
<span class="emoji" aria-label="thumbs up">👍</span><br/>
|
<span class="emoji" aria-label="thumbs up">👍</span><br/>
|
||||||
<a href="mailto:mail@domain.com" rel="nofollow">mail@domain.com</a><br/>
|
<a href="mailto:mail@domain.com" rel="nofollow">mail@domain.com</a><br/>
|
||||||
|
@ -772,9 +776,9 @@ space</p>
|
||||||
<a href="https://example.com/image.jpg" target="_blank" rel="nofollow noopener"><img src="https://example.com/image.jpg" alt="remote image"/></a><br/>
|
<a href="https://example.com/image.jpg" target="_blank" rel="nofollow noopener"><img src="https://example.com/image.jpg" alt="remote image"/></a><br/>
|
||||||
<a href="/relative/path/image.jpg" rel="nofollow"><img src="/relative/path/image.jpg" title="local image" alt="local image"/></a><br/>
|
<a href="/relative/path/image.jpg" rel="nofollow"><img src="/relative/path/image.jpg" title="local image" alt="local image"/></a><br/>
|
||||||
<a href="https://example.com/image.jpg" rel="nofollow"><img src="https://example.com/image.jpg" title="remote link" alt="remote link"/></a><br/>
|
<a href="https://example.com/image.jpg" rel="nofollow"><img src="https://example.com/image.jpg" title="remote link" alt="remote link"/></a><br/>
|
||||||
<a href="https://example.com/user/repo/compare/88fc37a3c0a4dda553bdcfc80c178a58247f42fb...12fc37a3c0a4dda553bdcfc80c178a58247f42fb#hash" rel="nofollow">https://example.com/user/repo/compare/88fc37a3c0a4dda553bdcfc80c178a58247f42fb...12fc37a3c0a4dda553bdcfc80c178a58247f42fb#hash</a><br/>
|
<a href="https://example.com/user/repo/compare/88fc37a3c0a4dda553bdcfc80c178a58247f42fb...12fc37a3c0a4dda553bdcfc80c178a58247f42fb#hash" rel="nofollow"><code>88fc37a3c0...12fc37a3c0 (hash)</code></a><br/>
|
||||||
com 88fc37a3c0a4dda553bdcfc80c178a58247f42fb...12fc37a3c0a4dda553bdcfc80c178a58247f42fb pare<br/>
|
com 88fc37a3c0a4dda553bdcfc80c178a58247f42fb...12fc37a3c0a4dda553bdcfc80c178a58247f42fb pare<br/>
|
||||||
<a href="https://example.com/user/repo/commit/88fc37a3c0a4dda553bdcfc80c178a58247f42fb" rel="nofollow">https://example.com/user/repo/commit/88fc37a3c0a4dda553bdcfc80c178a58247f42fb</a><br/>
|
<a href="https://example.com/user/repo/commit/88fc37a3c0a4dda553bdcfc80c178a58247f42fb" rel="nofollow"><code>88fc37a3c0</code></a><br/>
|
||||||
com 88fc37a3c0a4dda553bdcfc80c178a58247f42fb mit<br/>
|
com 88fc37a3c0a4dda553bdcfc80c178a58247f42fb mit<br/>
|
||||||
<span class="emoji" aria-label="thumbs up">👍</span><br/>
|
<span class="emoji" aria-label="thumbs up">👍</span><br/>
|
||||||
<a href="mailto:mail@domain.com" rel="nofollow">mail@domain.com</a><br/>
|
<a href="mailto:mail@domain.com" rel="nofollow">mail@domain.com</a><br/>
|
||||||
|
@ -801,9 +805,9 @@ space</p>
|
||||||
<a href="https://example.com/image.jpg" target="_blank" rel="nofollow noopener"><img src="https://example.com/image.jpg" alt="remote image"/></a><br/>
|
<a href="https://example.com/image.jpg" target="_blank" rel="nofollow noopener"><img src="https://example.com/image.jpg" alt="remote image"/></a><br/>
|
||||||
<a href="/relative/path/wiki/raw/image.jpg" rel="nofollow"><img src="/relative/path/wiki/raw/image.jpg" title="local image" alt="local image"/></a><br/>
|
<a href="/relative/path/wiki/raw/image.jpg" rel="nofollow"><img src="/relative/path/wiki/raw/image.jpg" title="local image" alt="local image"/></a><br/>
|
||||||
<a href="https://example.com/image.jpg" rel="nofollow"><img src="https://example.com/image.jpg" title="remote link" alt="remote link"/></a><br/>
|
<a href="https://example.com/image.jpg" rel="nofollow"><img src="https://example.com/image.jpg" title="remote link" alt="remote link"/></a><br/>
|
||||||
<a href="https://example.com/user/repo/compare/88fc37a3c0a4dda553bdcfc80c178a58247f42fb...12fc37a3c0a4dda553bdcfc80c178a58247f42fb#hash" rel="nofollow">https://example.com/user/repo/compare/88fc37a3c0a4dda553bdcfc80c178a58247f42fb...12fc37a3c0a4dda553bdcfc80c178a58247f42fb#hash</a><br/>
|
<a href="https://example.com/user/repo/compare/88fc37a3c0a4dda553bdcfc80c178a58247f42fb...12fc37a3c0a4dda553bdcfc80c178a58247f42fb#hash" rel="nofollow"><code>88fc37a3c0...12fc37a3c0 (hash)</code></a><br/>
|
||||||
com 88fc37a3c0a4dda553bdcfc80c178a58247f42fb...12fc37a3c0a4dda553bdcfc80c178a58247f42fb pare<br/>
|
com 88fc37a3c0a4dda553bdcfc80c178a58247f42fb...12fc37a3c0a4dda553bdcfc80c178a58247f42fb pare<br/>
|
||||||
<a href="https://example.com/user/repo/commit/88fc37a3c0a4dda553bdcfc80c178a58247f42fb" rel="nofollow">https://example.com/user/repo/commit/88fc37a3c0a4dda553bdcfc80c178a58247f42fb</a><br/>
|
<a href="https://example.com/user/repo/commit/88fc37a3c0a4dda553bdcfc80c178a58247f42fb" rel="nofollow"><code>88fc37a3c0</code></a><br/>
|
||||||
com 88fc37a3c0a4dda553bdcfc80c178a58247f42fb mit<br/>
|
com 88fc37a3c0a4dda553bdcfc80c178a58247f42fb mit<br/>
|
||||||
<span class="emoji" aria-label="thumbs up">👍</span><br/>
|
<span class="emoji" aria-label="thumbs up">👍</span><br/>
|
||||||
<a href="mailto:mail@domain.com" rel="nofollow">mail@domain.com</a><br/>
|
<a href="mailto:mail@domain.com" rel="nofollow">mail@domain.com</a><br/>
|
||||||
|
@ -831,9 +835,9 @@ space</p>
|
||||||
<a href="https://example.com/image.jpg" target="_blank" rel="nofollow noopener"><img src="https://example.com/image.jpg" alt="remote image"/></a><br/>
|
<a href="https://example.com/image.jpg" target="_blank" rel="nofollow noopener"><img src="https://example.com/image.jpg" alt="remote image"/></a><br/>
|
||||||
<a href="/user/repo/media/branch/main/image.jpg" rel="nofollow"><img src="/user/repo/media/branch/main/image.jpg" title="local image" alt="local image"/></a><br/>
|
<a href="/user/repo/media/branch/main/image.jpg" rel="nofollow"><img src="/user/repo/media/branch/main/image.jpg" title="local image" alt="local image"/></a><br/>
|
||||||
<a href="https://example.com/image.jpg" rel="nofollow"><img src="https://example.com/image.jpg" title="remote link" alt="remote link"/></a><br/>
|
<a href="https://example.com/image.jpg" rel="nofollow"><img src="https://example.com/image.jpg" title="remote link" alt="remote link"/></a><br/>
|
||||||
<a href="https://example.com/user/repo/compare/88fc37a3c0a4dda553bdcfc80c178a58247f42fb...12fc37a3c0a4dda553bdcfc80c178a58247f42fb#hash" rel="nofollow">https://example.com/user/repo/compare/88fc37a3c0a4dda553bdcfc80c178a58247f42fb...12fc37a3c0a4dda553bdcfc80c178a58247f42fb#hash</a><br/>
|
<a href="https://example.com/user/repo/compare/88fc37a3c0a4dda553bdcfc80c178a58247f42fb...12fc37a3c0a4dda553bdcfc80c178a58247f42fb#hash" rel="nofollow"><code>88fc37a3c0...12fc37a3c0 (hash)</code></a><br/>
|
||||||
com 88fc37a3c0a4dda553bdcfc80c178a58247f42fb...12fc37a3c0a4dda553bdcfc80c178a58247f42fb pare<br/>
|
com 88fc37a3c0a4dda553bdcfc80c178a58247f42fb...12fc37a3c0a4dda553bdcfc80c178a58247f42fb pare<br/>
|
||||||
<a href="https://example.com/user/repo/commit/88fc37a3c0a4dda553bdcfc80c178a58247f42fb" rel="nofollow">https://example.com/user/repo/commit/88fc37a3c0a4dda553bdcfc80c178a58247f42fb</a><br/>
|
<a href="https://example.com/user/repo/commit/88fc37a3c0a4dda553bdcfc80c178a58247f42fb" rel="nofollow"><code>88fc37a3c0</code></a><br/>
|
||||||
com 88fc37a3c0a4dda553bdcfc80c178a58247f42fb mit<br/>
|
com 88fc37a3c0a4dda553bdcfc80c178a58247f42fb mit<br/>
|
||||||
<span class="emoji" aria-label="thumbs up">👍</span><br/>
|
<span class="emoji" aria-label="thumbs up">👍</span><br/>
|
||||||
<a href="mailto:mail@domain.com" rel="nofollow">mail@domain.com</a><br/>
|
<a href="mailto:mail@domain.com" rel="nofollow">mail@domain.com</a><br/>
|
||||||
|
@ -861,9 +865,9 @@ space</p>
|
||||||
<a href="https://example.com/image.jpg" target="_blank" rel="nofollow noopener"><img src="https://example.com/image.jpg" alt="remote image"/></a><br/>
|
<a href="https://example.com/image.jpg" target="_blank" rel="nofollow noopener"><img src="https://example.com/image.jpg" alt="remote image"/></a><br/>
|
||||||
<a href="/relative/path/wiki/raw/image.jpg" rel="nofollow"><img src="/relative/path/wiki/raw/image.jpg" title="local image" alt="local image"/></a><br/>
|
<a href="/relative/path/wiki/raw/image.jpg" rel="nofollow"><img src="/relative/path/wiki/raw/image.jpg" title="local image" alt="local image"/></a><br/>
|
||||||
<a href="https://example.com/image.jpg" rel="nofollow"><img src="https://example.com/image.jpg" title="remote link" alt="remote link"/></a><br/>
|
<a href="https://example.com/image.jpg" rel="nofollow"><img src="https://example.com/image.jpg" title="remote link" alt="remote link"/></a><br/>
|
||||||
<a href="https://example.com/user/repo/compare/88fc37a3c0a4dda553bdcfc80c178a58247f42fb...12fc37a3c0a4dda553bdcfc80c178a58247f42fb#hash" rel="nofollow">https://example.com/user/repo/compare/88fc37a3c0a4dda553bdcfc80c178a58247f42fb...12fc37a3c0a4dda553bdcfc80c178a58247f42fb#hash</a><br/>
|
<a href="https://example.com/user/repo/compare/88fc37a3c0a4dda553bdcfc80c178a58247f42fb...12fc37a3c0a4dda553bdcfc80c178a58247f42fb#hash" rel="nofollow"><code>88fc37a3c0...12fc37a3c0 (hash)</code></a><br/>
|
||||||
com 88fc37a3c0a4dda553bdcfc80c178a58247f42fb...12fc37a3c0a4dda553bdcfc80c178a58247f42fb pare<br/>
|
com 88fc37a3c0a4dda553bdcfc80c178a58247f42fb...12fc37a3c0a4dda553bdcfc80c178a58247f42fb pare<br/>
|
||||||
<a href="https://example.com/user/repo/commit/88fc37a3c0a4dda553bdcfc80c178a58247f42fb" rel="nofollow">https://example.com/user/repo/commit/88fc37a3c0a4dda553bdcfc80c178a58247f42fb</a><br/>
|
<a href="https://example.com/user/repo/commit/88fc37a3c0a4dda553bdcfc80c178a58247f42fb" rel="nofollow"><code>88fc37a3c0</code></a><br/>
|
||||||
com 88fc37a3c0a4dda553bdcfc80c178a58247f42fb mit<br/>
|
com 88fc37a3c0a4dda553bdcfc80c178a58247f42fb mit<br/>
|
||||||
<span class="emoji" aria-label="thumbs up">👍</span><br/>
|
<span class="emoji" aria-label="thumbs up">👍</span><br/>
|
||||||
<a href="mailto:mail@domain.com" rel="nofollow">mail@domain.com</a><br/>
|
<a href="mailto:mail@domain.com" rel="nofollow">mail@domain.com</a><br/>
|
||||||
|
@ -891,9 +895,9 @@ space</p>
|
||||||
<a href="https://example.com/image.jpg" target="_blank" rel="nofollow noopener"><img src="https://example.com/image.jpg" alt="remote image"/></a><br/>
|
<a href="https://example.com/image.jpg" target="_blank" rel="nofollow noopener"><img src="https://example.com/image.jpg" alt="remote image"/></a><br/>
|
||||||
<a href="/user/repo/image.jpg" rel="nofollow"><img src="/user/repo/image.jpg" title="local image" alt="local image"/></a><br/>
|
<a href="/user/repo/image.jpg" rel="nofollow"><img src="/user/repo/image.jpg" title="local image" alt="local image"/></a><br/>
|
||||||
<a href="https://example.com/image.jpg" rel="nofollow"><img src="https://example.com/image.jpg" title="remote link" alt="remote link"/></a><br/>
|
<a href="https://example.com/image.jpg" rel="nofollow"><img src="https://example.com/image.jpg" title="remote link" alt="remote link"/></a><br/>
|
||||||
<a href="https://example.com/user/repo/compare/88fc37a3c0a4dda553bdcfc80c178a58247f42fb...12fc37a3c0a4dda553bdcfc80c178a58247f42fb#hash" rel="nofollow">https://example.com/user/repo/compare/88fc37a3c0a4dda553bdcfc80c178a58247f42fb...12fc37a3c0a4dda553bdcfc80c178a58247f42fb#hash</a><br/>
|
<a href="https://example.com/user/repo/compare/88fc37a3c0a4dda553bdcfc80c178a58247f42fb...12fc37a3c0a4dda553bdcfc80c178a58247f42fb#hash" rel="nofollow"><code>88fc37a3c0...12fc37a3c0 (hash)</code></a><br/>
|
||||||
com 88fc37a3c0a4dda553bdcfc80c178a58247f42fb...12fc37a3c0a4dda553bdcfc80c178a58247f42fb pare<br/>
|
com 88fc37a3c0a4dda553bdcfc80c178a58247f42fb...12fc37a3c0a4dda553bdcfc80c178a58247f42fb pare<br/>
|
||||||
<a href="https://example.com/user/repo/commit/88fc37a3c0a4dda553bdcfc80c178a58247f42fb" rel="nofollow">https://example.com/user/repo/commit/88fc37a3c0a4dda553bdcfc80c178a58247f42fb</a><br/>
|
<a href="https://example.com/user/repo/commit/88fc37a3c0a4dda553bdcfc80c178a58247f42fb" rel="nofollow"><code>88fc37a3c0</code></a><br/>
|
||||||
com 88fc37a3c0a4dda553bdcfc80c178a58247f42fb mit<br/>
|
com 88fc37a3c0a4dda553bdcfc80c178a58247f42fb mit<br/>
|
||||||
<span class="emoji" aria-label="thumbs up">👍</span><br/>
|
<span class="emoji" aria-label="thumbs up">👍</span><br/>
|
||||||
<a href="mailto:mail@domain.com" rel="nofollow">mail@domain.com</a><br/>
|
<a href="mailto:mail@domain.com" rel="nofollow">mail@domain.com</a><br/>
|
||||||
|
@ -921,9 +925,9 @@ space</p>
|
||||||
<a href="https://example.com/image.jpg" target="_blank" rel="nofollow noopener"><img src="https://example.com/image.jpg" alt="remote image"/></a><br/>
|
<a href="https://example.com/image.jpg" target="_blank" rel="nofollow noopener"><img src="https://example.com/image.jpg" alt="remote image"/></a><br/>
|
||||||
<a href="/relative/path/wiki/raw/image.jpg" rel="nofollow"><img src="/relative/path/wiki/raw/image.jpg" title="local image" alt="local image"/></a><br/>
|
<a href="/relative/path/wiki/raw/image.jpg" rel="nofollow"><img src="/relative/path/wiki/raw/image.jpg" title="local image" alt="local image"/></a><br/>
|
||||||
<a href="https://example.com/image.jpg" rel="nofollow"><img src="https://example.com/image.jpg" title="remote link" alt="remote link"/></a><br/>
|
<a href="https://example.com/image.jpg" rel="nofollow"><img src="https://example.com/image.jpg" title="remote link" alt="remote link"/></a><br/>
|
||||||
<a href="https://example.com/user/repo/compare/88fc37a3c0a4dda553bdcfc80c178a58247f42fb...12fc37a3c0a4dda553bdcfc80c178a58247f42fb#hash" rel="nofollow">https://example.com/user/repo/compare/88fc37a3c0a4dda553bdcfc80c178a58247f42fb...12fc37a3c0a4dda553bdcfc80c178a58247f42fb#hash</a><br/>
|
<a href="https://example.com/user/repo/compare/88fc37a3c0a4dda553bdcfc80c178a58247f42fb...12fc37a3c0a4dda553bdcfc80c178a58247f42fb#hash" rel="nofollow"><code>88fc37a3c0...12fc37a3c0 (hash)</code></a><br/>
|
||||||
com 88fc37a3c0a4dda553bdcfc80c178a58247f42fb...12fc37a3c0a4dda553bdcfc80c178a58247f42fb pare<br/>
|
com 88fc37a3c0a4dda553bdcfc80c178a58247f42fb...12fc37a3c0a4dda553bdcfc80c178a58247f42fb pare<br/>
|
||||||
<a href="https://example.com/user/repo/commit/88fc37a3c0a4dda553bdcfc80c178a58247f42fb" rel="nofollow">https://example.com/user/repo/commit/88fc37a3c0a4dda553bdcfc80c178a58247f42fb</a><br/>
|
<a href="https://example.com/user/repo/commit/88fc37a3c0a4dda553bdcfc80c178a58247f42fb" rel="nofollow"><code>88fc37a3c0</code></a><br/>
|
||||||
com 88fc37a3c0a4dda553bdcfc80c178a58247f42fb mit<br/>
|
com 88fc37a3c0a4dda553bdcfc80c178a58247f42fb mit<br/>
|
||||||
<span class="emoji" aria-label="thumbs up">👍</span><br/>
|
<span class="emoji" aria-label="thumbs up">👍</span><br/>
|
||||||
<a href="mailto:mail@domain.com" rel="nofollow">mail@domain.com</a><br/>
|
<a href="mailto:mail@domain.com" rel="nofollow">mail@domain.com</a><br/>
|
||||||
|
@ -952,9 +956,9 @@ space</p>
|
||||||
<a href="https://example.com/image.jpg" target="_blank" rel="nofollow noopener"><img src="https://example.com/image.jpg" alt="remote image"/></a><br/>
|
<a href="https://example.com/image.jpg" target="_blank" rel="nofollow noopener"><img src="https://example.com/image.jpg" alt="remote image"/></a><br/>
|
||||||
<a href="/user/repo/media/branch/main/sub/folder/image.jpg" rel="nofollow"><img src="/user/repo/media/branch/main/sub/folder/image.jpg" title="local image" alt="local image"/></a><br/>
|
<a href="/user/repo/media/branch/main/sub/folder/image.jpg" rel="nofollow"><img src="/user/repo/media/branch/main/sub/folder/image.jpg" title="local image" alt="local image"/></a><br/>
|
||||||
<a href="https://example.com/image.jpg" rel="nofollow"><img src="https://example.com/image.jpg" title="remote link" alt="remote link"/></a><br/>
|
<a href="https://example.com/image.jpg" rel="nofollow"><img src="https://example.com/image.jpg" title="remote link" alt="remote link"/></a><br/>
|
||||||
<a href="https://example.com/user/repo/compare/88fc37a3c0a4dda553bdcfc80c178a58247f42fb...12fc37a3c0a4dda553bdcfc80c178a58247f42fb#hash" rel="nofollow">https://example.com/user/repo/compare/88fc37a3c0a4dda553bdcfc80c178a58247f42fb...12fc37a3c0a4dda553bdcfc80c178a58247f42fb#hash</a><br/>
|
<a href="https://example.com/user/repo/compare/88fc37a3c0a4dda553bdcfc80c178a58247f42fb...12fc37a3c0a4dda553bdcfc80c178a58247f42fb#hash" rel="nofollow"><code>88fc37a3c0...12fc37a3c0 (hash)</code></a><br/>
|
||||||
com 88fc37a3c0a4dda553bdcfc80c178a58247f42fb...12fc37a3c0a4dda553bdcfc80c178a58247f42fb pare<br/>
|
com 88fc37a3c0a4dda553bdcfc80c178a58247f42fb...12fc37a3c0a4dda553bdcfc80c178a58247f42fb pare<br/>
|
||||||
<a href="https://example.com/user/repo/commit/88fc37a3c0a4dda553bdcfc80c178a58247f42fb" rel="nofollow">https://example.com/user/repo/commit/88fc37a3c0a4dda553bdcfc80c178a58247f42fb</a><br/>
|
<a href="https://example.com/user/repo/commit/88fc37a3c0a4dda553bdcfc80c178a58247f42fb" rel="nofollow"><code>88fc37a3c0</code></a><br/>
|
||||||
com 88fc37a3c0a4dda553bdcfc80c178a58247f42fb mit<br/>
|
com 88fc37a3c0a4dda553bdcfc80c178a58247f42fb mit<br/>
|
||||||
<span class="emoji" aria-label="thumbs up">👍</span><br/>
|
<span class="emoji" aria-label="thumbs up">👍</span><br/>
|
||||||
<a href="mailto:mail@domain.com" rel="nofollow">mail@domain.com</a><br/>
|
<a href="mailto:mail@domain.com" rel="nofollow">mail@domain.com</a><br/>
|
||||||
|
@ -983,9 +987,9 @@ space</p>
|
||||||
<a href="https://example.com/image.jpg" target="_blank" rel="nofollow noopener"><img src="https://example.com/image.jpg" alt="remote image"/></a><br/>
|
<a href="https://example.com/image.jpg" target="_blank" rel="nofollow noopener"><img src="https://example.com/image.jpg" alt="remote image"/></a><br/>
|
||||||
<a href="/relative/path/wiki/raw/image.jpg" rel="nofollow"><img src="/relative/path/wiki/raw/image.jpg" title="local image" alt="local image"/></a><br/>
|
<a href="/relative/path/wiki/raw/image.jpg" rel="nofollow"><img src="/relative/path/wiki/raw/image.jpg" title="local image" alt="local image"/></a><br/>
|
||||||
<a href="https://example.com/image.jpg" rel="nofollow"><img src="https://example.com/image.jpg" title="remote link" alt="remote link"/></a><br/>
|
<a href="https://example.com/image.jpg" rel="nofollow"><img src="https://example.com/image.jpg" title="remote link" alt="remote link"/></a><br/>
|
||||||
<a href="https://example.com/user/repo/compare/88fc37a3c0a4dda553bdcfc80c178a58247f42fb...12fc37a3c0a4dda553bdcfc80c178a58247f42fb#hash" rel="nofollow">https://example.com/user/repo/compare/88fc37a3c0a4dda553bdcfc80c178a58247f42fb...12fc37a3c0a4dda553bdcfc80c178a58247f42fb#hash</a><br/>
|
<a href="https://example.com/user/repo/compare/88fc37a3c0a4dda553bdcfc80c178a58247f42fb...12fc37a3c0a4dda553bdcfc80c178a58247f42fb#hash" rel="nofollow"><code>88fc37a3c0...12fc37a3c0 (hash)</code></a><br/>
|
||||||
com 88fc37a3c0a4dda553bdcfc80c178a58247f42fb...12fc37a3c0a4dda553bdcfc80c178a58247f42fb pare<br/>
|
com 88fc37a3c0a4dda553bdcfc80c178a58247f42fb...12fc37a3c0a4dda553bdcfc80c178a58247f42fb pare<br/>
|
||||||
<a href="https://example.com/user/repo/commit/88fc37a3c0a4dda553bdcfc80c178a58247f42fb" rel="nofollow">https://example.com/user/repo/commit/88fc37a3c0a4dda553bdcfc80c178a58247f42fb</a><br/>
|
<a href="https://example.com/user/repo/commit/88fc37a3c0a4dda553bdcfc80c178a58247f42fb" rel="nofollow"><code>88fc37a3c0</code></a><br/>
|
||||||
com 88fc37a3c0a4dda553bdcfc80c178a58247f42fb mit<br/>
|
com 88fc37a3c0a4dda553bdcfc80c178a58247f42fb mit<br/>
|
||||||
<span class="emoji" aria-label="thumbs up">👍</span><br/>
|
<span class="emoji" aria-label="thumbs up">👍</span><br/>
|
||||||
<a href="mailto:mail@domain.com" rel="nofollow">mail@domain.com</a><br/>
|
<a href="mailto:mail@domain.com" rel="nofollow">mail@domain.com</a><br/>
|
||||||
|
@ -996,11 +1000,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,
|
||||||
|
Metas: util.Iif(c.IsWiki, map[string]string{"markupContentMode": "wiki"}, map[string]string{}),
|
||||||
|
}, 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.IsMarkupContentWiki()),
|
||||||
strings.TrimLeft(string(v.Destination), "/"),
|
strings.TrimLeft(string(v.Destination), "/"),
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
|
@ -144,14 +144,14 @@ func (r *Writer) resolveLink(kind, link string) string {
|
||||||
}
|
}
|
||||||
|
|
||||||
base := r.Ctx.Links.Base
|
base := r.Ctx.Links.Base
|
||||||
if r.Ctx.IsWiki {
|
if r.Ctx.IsMarkupContentWiki() {
|
||||||
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(r.Ctx.IsMarkupContentWiki())
|
||||||
}
|
}
|
||||||
|
|
||||||
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,
|
Metas: map[string]string{"markupContentMode": util.Iif(isWiki, "wiki", "")},
|
||||||
}, 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,37 @@ const (
|
||||||
RenderMetaAsTable RenderMetaMode = "table"
|
RenderMetaAsTable RenderMetaMode = "table"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
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
|
|
||||||
|
Links Links // special link references for rendering, especially when there is a branch/tree path
|
||||||
|
|
||||||
|
// user&repo, format&style®exp (for external issue pattern), teams&org (for mention)
|
||||||
|
// BranchNameSubURL (for iframe&asciicast)
|
||||||
|
// markupAllowShortIssuePattern, markupContentMode (wiki)
|
||||||
|
// markdownLineBreakStyle (comment, document)
|
||||||
|
Metas map[string]string
|
||||||
|
|
||||||
GitRepo *git.Repository
|
GitRepo *git.Repository
|
||||||
Repo gitrepo.Repository
|
Repo gitrepo.Repository
|
||||||
ShaExistCache map[string]bool
|
ShaExistCache map[string]bool
|
||||||
|
@ -75,14 +95,35 @@ func (ctx *RenderContext) AddCancel(fn func()) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (ctx *RenderContext) IsMarkupContentWiki() bool {
|
||||||
|
return ctx.Metas != nil && ctx.Metas["markupContentMode"] == "wiki"
|
||||||
|
}
|
||||||
|
|
||||||
// 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 +211,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 {
|
||||||
|
@ -224,3 +229,7 @@ func Init(ph *ProcessorHelper) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func ComposeSimpleDocumentMetas() map[string]string {
|
||||||
|
return map[string]string{"markdownLineBreakStyle": "document"}
|
||||||
|
}
|
||||||
|
|
|
@ -10,7 +10,7 @@ import (
|
||||||
|
|
||||||
type Links struct {
|
type Links struct {
|
||||||
AbsolutePrefix bool // add absolute URL prefix to auto-resolved links like "#issue", but not for pre-provided links and medias
|
AbsolutePrefix bool // add absolute URL prefix to auto-resolved links like "#issue", but not for pre-provided links and medias
|
||||||
Base string // base prefix for pre-provided links and medias (images, videos)
|
Base string // base prefix for pre-provided links and medias (images, videos), usually it is the path to the repo
|
||||||
BranchPath string // actually it is the ref path, eg: "branch/features/feat-12", "tag/v1.0"
|
BranchPath string // actually it is the ref path, eg: "branch/features/feat-12", "tag/v1.0"
|
||||||
TreePath string // the dir of the file, eg: "doc" if the file "doc/CHANGE.md" is being rendered
|
TreePath string // the dir of the file, eg: "doc" if the file "doc/CHANGE.md" is being rendered
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,6 +5,7 @@ package queue
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"errors"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
@ -32,6 +33,7 @@ type ManagedWorkerPoolQueue interface {
|
||||||
|
|
||||||
// FlushWithContext tries to make the handler process all items in the queue synchronously.
|
// FlushWithContext tries to make the handler process all items in the queue synchronously.
|
||||||
// It is for testing purpose only. It's not designed to be used in a cluster.
|
// It is for testing purpose only. It's not designed to be used in a cluster.
|
||||||
|
// Negative timeout means discarding all items in the queue.
|
||||||
FlushWithContext(ctx context.Context, timeout time.Duration) error
|
FlushWithContext(ctx context.Context, timeout time.Duration) error
|
||||||
|
|
||||||
// RemoveAllItems removes all items in the base queue (on-the-fly items are not affected)
|
// RemoveAllItems removes all items in the base queue (on-the-fly items are not affected)
|
||||||
|
@ -76,15 +78,16 @@ func (m *Manager) ManagedQueues() map[int64]ManagedWorkerPoolQueue {
|
||||||
|
|
||||||
// FlushAll tries to make all managed queues process all items synchronously, until timeout or the queue is empty.
|
// FlushAll tries to make all managed queues process all items synchronously, until timeout or the queue is empty.
|
||||||
// It is for testing purpose only. It's not designed to be used in a cluster.
|
// It is for testing purpose only. It's not designed to be used in a cluster.
|
||||||
|
// Negative timeout means discarding all items in the queue.
|
||||||
func (m *Manager) FlushAll(ctx context.Context, timeout time.Duration) error {
|
func (m *Manager) FlushAll(ctx context.Context, timeout time.Duration) error {
|
||||||
var finalErr error
|
var finalErrors []error
|
||||||
qs := m.ManagedQueues()
|
qs := m.ManagedQueues()
|
||||||
for _, q := range qs {
|
for _, q := range qs {
|
||||||
if err := q.FlushWithContext(ctx, timeout); err != nil {
|
if err := q.FlushWithContext(ctx, timeout); err != nil {
|
||||||
finalErr = err // TODO: in Go 1.20: errors.Join
|
finalErrors = append(finalErrors, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return finalErr
|
return errors.Join(finalErrors...)
|
||||||
}
|
}
|
||||||
|
|
||||||
// CreateSimpleQueue creates a simple queue from global setting config provider by name
|
// CreateSimpleQueue creates a simple queue from global setting config provider by name
|
||||||
|
|
|
@ -23,7 +23,7 @@ var (
|
||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
unhandledItemRequeueDuration.Store(int64(5 * time.Second))
|
unhandledItemRequeueDuration.Store(int64(time.Second))
|
||||||
}
|
}
|
||||||
|
|
||||||
// workerGroup is a group of workers to work with a WorkerPoolQueue
|
// workerGroup is a group of workers to work with a WorkerPoolQueue
|
||||||
|
@ -104,7 +104,12 @@ func (q *WorkerPoolQueue[T]) doWorkerHandle(batch []T) {
|
||||||
// if none of the items were handled, it should back-off for a few seconds
|
// if none of the items were handled, it should back-off for a few seconds
|
||||||
// in this case the handler (eg: document indexer) may have encountered some errors/failures
|
// in this case the handler (eg: document indexer) may have encountered some errors/failures
|
||||||
if len(unhandled) == len(batch) && unhandledItemRequeueDuration.Load() != 0 {
|
if len(unhandled) == len(batch) && unhandledItemRequeueDuration.Load() != 0 {
|
||||||
|
if q.isFlushing.Load() {
|
||||||
|
return // do not requeue items when flushing, since all items failed, requeue them will continue failing.
|
||||||
|
}
|
||||||
log.Error("Queue %q failed to handle batch of %d items, backoff for a few seconds", q.GetName(), len(batch))
|
log.Error("Queue %q failed to handle batch of %d items, backoff for a few seconds", q.GetName(), len(batch))
|
||||||
|
// TODO: ideally it shouldn't "sleep" here (blocks the worker, then blocks flush).
|
||||||
|
// It could debounce the requeue operation, and try to requeue the items in the future.
|
||||||
select {
|
select {
|
||||||
case <-q.ctxRun.Done():
|
case <-q.ctxRun.Done():
|
||||||
case <-time.After(time.Duration(unhandledItemRequeueDuration.Load())):
|
case <-time.After(time.Duration(unhandledItemRequeueDuration.Load())):
|
||||||
|
@ -193,19 +198,37 @@ func (q *WorkerPoolQueue[T]) doStartNewWorker(wp *workerGroup[T]) {
|
||||||
// doFlush flushes the queue: it tries to read all items from the queue and handles them.
|
// doFlush flushes the queue: it tries to read all items from the queue and handles them.
|
||||||
// It is for testing purpose only. It's not designed to work for a cluster.
|
// It is for testing purpose only. It's not designed to work for a cluster.
|
||||||
func (q *WorkerPoolQueue[T]) doFlush(wg *workerGroup[T], flush flushType) {
|
func (q *WorkerPoolQueue[T]) doFlush(wg *workerGroup[T], flush flushType) {
|
||||||
|
q.isFlushing.Store(true)
|
||||||
|
defer q.isFlushing.Store(false)
|
||||||
|
|
||||||
log.Debug("Queue %q starts flushing", q.GetName())
|
log.Debug("Queue %q starts flushing", q.GetName())
|
||||||
defer log.Debug("Queue %q finishes flushing", q.GetName())
|
defer log.Debug("Queue %q finishes flushing", q.GetName())
|
||||||
|
|
||||||
// stop all workers, and prepare a new worker context to start new workers
|
// stop all workers, and prepare a new worker context to start new workers
|
||||||
|
|
||||||
wg.ctxWorkerCancel()
|
wg.ctxWorkerCancel()
|
||||||
wg.wg.Wait()
|
wg.wg.Wait()
|
||||||
|
|
||||||
defer func() {
|
defer func() {
|
||||||
close(flush)
|
close(flush.c)
|
||||||
wg.doPrepareWorkerContext()
|
wg.doPrepareWorkerContext()
|
||||||
}()
|
}()
|
||||||
|
|
||||||
|
if flush.timeout < 0 {
|
||||||
|
// discard everything
|
||||||
|
wg.batchBuffer = nil
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-wg.popItemChan:
|
||||||
|
case <-wg.popItemErr:
|
||||||
|
case <-q.batchChan:
|
||||||
|
case <-q.ctxRun.Done():
|
||||||
|
return
|
||||||
|
default:
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// drain the batch channel first
|
// drain the batch channel first
|
||||||
loop:
|
loop:
|
||||||
for {
|
for {
|
||||||
|
@ -221,6 +244,9 @@ loop:
|
||||||
emptyCounter := 0
|
emptyCounter := 0
|
||||||
for {
|
for {
|
||||||
select {
|
select {
|
||||||
|
case <-q.ctxRun.Done():
|
||||||
|
log.Debug("Queue %q is shutting down", q.GetName())
|
||||||
|
return
|
||||||
case data, dataOk := <-wg.popItemChan:
|
case data, dataOk := <-wg.popItemChan:
|
||||||
if !dataOk {
|
if !dataOk {
|
||||||
return
|
return
|
||||||
|
@ -236,9 +262,6 @@ loop:
|
||||||
log.Error("Failed to pop item from queue %q (doFlush): %v", q.GetName(), err)
|
log.Error("Failed to pop item from queue %q (doFlush): %v", q.GetName(), err)
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
case <-q.ctxRun.Done():
|
|
||||||
log.Debug("Queue %q is shutting down", q.GetName())
|
|
||||||
return
|
|
||||||
case <-time.After(20 * time.Millisecond):
|
case <-time.After(20 * time.Millisecond):
|
||||||
// There is no reliable way to make sure all queue items are consumed by the Flush, there always might be some items stored in some buffers/temp variables.
|
// There is no reliable way to make sure all queue items are consumed by the Flush, there always might be some items stored in some buffers/temp variables.
|
||||||
// If we run Gitea in a cluster, we can even not guarantee all items are consumed in a deterministic instance.
|
// If we run Gitea in a cluster, we can even not guarantee all items are consumed in a deterministic instance.
|
||||||
|
@ -316,6 +339,15 @@ func (q *WorkerPoolQueue[T]) doRun() {
|
||||||
var batchDispatchC <-chan time.Time = infiniteTimerC
|
var batchDispatchC <-chan time.Time = infiniteTimerC
|
||||||
for {
|
for {
|
||||||
select {
|
select {
|
||||||
|
case flush := <-q.flushChan:
|
||||||
|
// before flushing, it needs to try to dispatch the batch to worker first, in case there is no worker running
|
||||||
|
// after the flushing, there is at least one worker running, so "doFlush" could wait for workers to finish
|
||||||
|
// since we are already in a "flush" operation, so the dispatching function shouldn't read the flush chan.
|
||||||
|
q.doDispatchBatchToWorker(wg, skipFlushChan)
|
||||||
|
q.doFlush(wg, flush)
|
||||||
|
case <-q.ctxRun.Done():
|
||||||
|
log.Debug("Queue %q is shutting down", q.GetName())
|
||||||
|
return
|
||||||
case data, dataOk := <-wg.popItemChan:
|
case data, dataOk := <-wg.popItemChan:
|
||||||
if !dataOk {
|
if !dataOk {
|
||||||
return
|
return
|
||||||
|
@ -334,20 +366,11 @@ func (q *WorkerPoolQueue[T]) doRun() {
|
||||||
case <-batchDispatchC:
|
case <-batchDispatchC:
|
||||||
batchDispatchC = infiniteTimerC
|
batchDispatchC = infiniteTimerC
|
||||||
q.doDispatchBatchToWorker(wg, q.flushChan)
|
q.doDispatchBatchToWorker(wg, q.flushChan)
|
||||||
case flush := <-q.flushChan:
|
|
||||||
// before flushing, it needs to try to dispatch the batch to worker first, in case there is no worker running
|
|
||||||
// after the flushing, there is at least one worker running, so "doFlush" could wait for workers to finish
|
|
||||||
// since we are already in a "flush" operation, so the dispatching function shouldn't read the flush chan.
|
|
||||||
q.doDispatchBatchToWorker(wg, skipFlushChan)
|
|
||||||
q.doFlush(wg, flush)
|
|
||||||
case err := <-wg.popItemErr:
|
case err := <-wg.popItemErr:
|
||||||
if !q.isCtxRunCanceled() {
|
if !q.isCtxRunCanceled() {
|
||||||
log.Error("Failed to pop item from queue %q (doRun): %v", q.GetName(), err)
|
log.Error("Failed to pop item from queue %q (doRun): %v", q.GetName(), err)
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
case <-q.ctxRun.Done():
|
|
||||||
log.Debug("Queue %q is shutting down", q.GetName())
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -32,8 +32,9 @@ type WorkerPoolQueue[T any] struct {
|
||||||
baseConfig *BaseConfig
|
baseConfig *BaseConfig
|
||||||
baseQueue baseQueue
|
baseQueue baseQueue
|
||||||
|
|
||||||
batchChan chan []T
|
batchChan chan []T
|
||||||
flushChan chan flushType
|
flushChan chan flushType
|
||||||
|
isFlushing atomic.Bool
|
||||||
|
|
||||||
batchLength int
|
batchLength int
|
||||||
workerNum int
|
workerNum int
|
||||||
|
@ -42,7 +43,10 @@ type WorkerPoolQueue[T any] struct {
|
||||||
workerNumMu sync.Mutex
|
workerNumMu sync.Mutex
|
||||||
}
|
}
|
||||||
|
|
||||||
type flushType chan struct{}
|
type flushType struct {
|
||||||
|
timeout time.Duration
|
||||||
|
c chan struct{}
|
||||||
|
}
|
||||||
|
|
||||||
var _ ManagedWorkerPoolQueue = (*WorkerPoolQueue[any])(nil)
|
var _ ManagedWorkerPoolQueue = (*WorkerPoolQueue[any])(nil)
|
||||||
|
|
||||||
|
@ -104,12 +108,12 @@ func (q *WorkerPoolQueue[T]) FlushWithContext(ctx context.Context, timeout time.
|
||||||
if timeout > 0 {
|
if timeout > 0 {
|
||||||
after = time.After(timeout)
|
after = time.After(timeout)
|
||||||
}
|
}
|
||||||
c := make(flushType)
|
flush := flushType{timeout: timeout, c: make(chan struct{})}
|
||||||
|
|
||||||
// send flush request
|
// send flush request
|
||||||
// if it blocks, it means that there is a flush in progress or the queue hasn't been started yet
|
// if it blocks, it means that there is a flush in progress or the queue hasn't been started yet
|
||||||
select {
|
select {
|
||||||
case q.flushChan <- c:
|
case q.flushChan <- flush:
|
||||||
case <-ctx.Done():
|
case <-ctx.Done():
|
||||||
return ctx.Err()
|
return ctx.Err()
|
||||||
case <-q.ctxRun.Done():
|
case <-q.ctxRun.Done():
|
||||||
|
@ -120,7 +124,7 @@ func (q *WorkerPoolQueue[T]) FlushWithContext(ctx context.Context, timeout time.
|
||||||
|
|
||||||
// wait for flush to finish
|
// wait for flush to finish
|
||||||
select {
|
select {
|
||||||
case <-c:
|
case <-flush.c:
|
||||||
return nil
|
return nil
|
||||||
case <-ctx.Done():
|
case <-ctx.Done():
|
||||||
return ctx.Err()
|
return ctx.Err()
|
||||||
|
|
|
@ -38,8 +38,8 @@ func TestGetDirectorySize(t *testing.T) {
|
||||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||||
repo, err := repo_model.GetRepositoryByID(db.DefaultContext, 1)
|
repo, err := repo_model.GetRepositoryByID(db.DefaultContext, 1)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
|
|
||||||
size, err := getDirectorySize(repo.RepoPath())
|
size, err := getDirectorySize(repo.RepoPath())
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
assert.EqualValues(t, size, repo.Size)
|
repo.Size = 8165 // real size on the disk
|
||||||
|
assert.EqualValues(t, repo.Size, size)
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,33 +3,33 @@
|
||||||
|
|
||||||
package setting
|
package setting
|
||||||
|
|
||||||
// Attachment settings
|
type AttachmentSettingType struct {
|
||||||
var Attachment = struct {
|
|
||||||
Storage *Storage
|
Storage *Storage
|
||||||
AllowedTypes string
|
AllowedTypes string
|
||||||
MaxSize int64
|
MaxSize int64
|
||||||
MaxFiles int
|
MaxFiles int
|
||||||
Enabled bool
|
Enabled bool
|
||||||
}{
|
|
||||||
Storage: &Storage{},
|
|
||||||
AllowedTypes: ".cpuprofile,.csv,.dmp,.docx,.fodg,.fodp,.fods,.fodt,.gif,.gz,.jpeg,.jpg,.json,.jsonc,.log,.md,.mov,.mp4,.odf,.odg,.odp,.ods,.odt,.patch,.pdf,.png,.pptx,.svg,.tgz,.txt,.webm,.xls,.xlsx,.zip",
|
|
||||||
MaxSize: 2048,
|
|
||||||
MaxFiles: 5,
|
|
||||||
Enabled: true,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var Attachment AttachmentSettingType
|
||||||
|
|
||||||
func loadAttachmentFrom(rootCfg ConfigProvider) (err error) {
|
func loadAttachmentFrom(rootCfg ConfigProvider) (err error) {
|
||||||
|
Attachment = AttachmentSettingType{
|
||||||
|
AllowedTypes: ".avif,.cpuprofile,.csv,.dmp,.docx,.fodg,.fodp,.fods,.fodt,.gif,.gz,.jpeg,.jpg,.json,.jsonc,.log,.md,.mov,.mp4,.odf,.odg,.odp,.ods,.odt,.patch,.pdf,.png,.pptx,.svg,.tgz,.txt,.webm,.webp,.xls,.xlsx,.zip",
|
||||||
|
MaxSize: 2048,
|
||||||
|
MaxFiles: 5,
|
||||||
|
Enabled: true,
|
||||||
|
}
|
||||||
sec, _ := rootCfg.GetSection("attachment")
|
sec, _ := rootCfg.GetSection("attachment")
|
||||||
if sec == nil {
|
if sec == nil {
|
||||||
Attachment.Storage, err = getStorage(rootCfg, "attachments", "", nil)
|
Attachment.Storage, err = getStorage(rootCfg, "attachments", "", nil)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
Attachment.AllowedTypes = sec.Key("ALLOWED_TYPES").MustString(".cpuprofile,.csv,.dmp,.docx,.fodg,.fodp,.fods,.fodt,.gif,.gz,.jpeg,.jpg,.json,.jsonc,.log,.md,.mov,.mp4,.odf,.odg,.odp,.ods,.odt,.patch,.pdf,.png,.pptx,.svg,.tgz,.txt,.webm,.xls,.xlsx,.zip")
|
Attachment.AllowedTypes = sec.Key("ALLOWED_TYPES").MustString(Attachment.AllowedTypes)
|
||||||
Attachment.MaxSize = sec.Key("MAX_SIZE").MustInt64(2048)
|
Attachment.MaxSize = sec.Key("MAX_SIZE").MustInt64(Attachment.MaxSize)
|
||||||
Attachment.MaxFiles = sec.Key("MAX_FILES").MustInt(5)
|
Attachment.MaxFiles = sec.Key("MAX_FILES").MustInt(Attachment.MaxFiles)
|
||||||
Attachment.Enabled = sec.Key("ENABLED").MustBool(true)
|
Attachment.Enabled = sec.Key("ENABLED").MustBool(Attachment.Enabled)
|
||||||
|
|
||||||
Attachment.Storage, err = getStorage(rootCfg, "attachments", "", sec)
|
Attachment.Storage, err = getStorage(rootCfg, "attachments", "", sec)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
|
@ -86,6 +86,7 @@ var UI = struct {
|
||||||
Reactions: []string{`+1`, `-1`, `laugh`, `hooray`, `confused`, `heart`, `rocket`, `eyes`},
|
Reactions: []string{`+1`, `-1`, `laugh`, `hooray`, `confused`, `heart`, `rocket`, `eyes`},
|
||||||
CustomEmojis: []string{`git`, `gitea`, `codeberg`, `gitlab`, `github`, `gogs`},
|
CustomEmojis: []string{`git`, `gitea`, `codeberg`, `gitlab`, `github`, `gogs`},
|
||||||
CustomEmojisMap: map[string]string{"git": ":git:", "gitea": ":gitea:", "codeberg": ":codeberg:", "gitlab": ":gitlab:", "github": ":github:", "gogs": ":gogs:"},
|
CustomEmojisMap: map[string]string{"git": ":git:", "gitea": ":gitea:", "codeberg": ":codeberg:", "gitlab": ":gitlab:", "github": ":github:", "gogs": ":gogs:"},
|
||||||
|
ExploreDefaultSort: "recentupdate",
|
||||||
PreferredTimestampTense: "mixed",
|
PreferredTimestampTense: "mixed",
|
||||||
|
|
||||||
AmbiguousUnicodeDetection: true,
|
AmbiguousUnicodeDetection: true,
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -62,19 +62,18 @@ func (ut *RenderUtils) RenderCommitMessageLinkSubject(msg, urlDefault string, me
|
||||||
}
|
}
|
||||||
msgLine = strings.TrimRightFunc(msgLine, unicode.IsSpace)
|
msgLine = strings.TrimRightFunc(msgLine, unicode.IsSpace)
|
||||||
if len(msgLine) == 0 {
|
if len(msgLine) == 0 {
|
||||||
return template.HTML("")
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
// we can safely assume that it will not return any error, since there
|
// we can safely assume that it will not return any error, since there
|
||||||
// shouldn't be any special HTML.
|
// shouldn't be any special HTML.
|
||||||
renderedMessage, err := markup.RenderCommitMessageSubject(&markup.RenderContext{
|
renderedMessage, err := markup.RenderCommitMessageSubject(&markup.RenderContext{
|
||||||
Ctx: ut.ctx,
|
Ctx: ut.ctx,
|
||||||
DefaultLink: urlDefault,
|
Metas: metas,
|
||||||
Metas: metas,
|
}, urlDefault, template.HTMLEscapeString(msgLine))
|
||||||
}, template.HTMLEscapeString(msgLine))
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error("RenderCommitMessageSubject: %v", err)
|
log.Error("RenderCommitMessageSubject: %v", err)
|
||||||
return template.HTML("")
|
return ""
|
||||||
}
|
}
|
||||||
return renderCodeBlock(template.HTML(renderedMessage))
|
return renderCodeBlock(template.HTML(renderedMessage))
|
||||||
}
|
}
|
||||||
|
@ -210,7 +209,7 @@ func reactionToEmoji(reaction string) template.HTML {
|
||||||
func (ut *RenderUtils) MarkdownToHtml(input string) template.HTML { //nolint:revive
|
func (ut *RenderUtils) MarkdownToHtml(input string) template.HTML { //nolint:revive
|
||||||
output, err := markdown.RenderString(&markup.RenderContext{
|
output, err := markdown.RenderString(&markup.RenderContext{
|
||||||
Ctx: ut.ctx,
|
Ctx: ut.ctx,
|
||||||
Metas: map[string]string{"mode": "document"},
|
Metas: markup.ComposeSimpleDocumentMetas(),
|
||||||
}, input)
|
}, input)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error("RenderString: %v", err)
|
log.Error("RenderString: %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"
|
||||||
|
@ -46,10 +47,11 @@ mail@domain.com
|
||||||
}
|
}
|
||||||
|
|
||||||
var testMetas = map[string]string{
|
var testMetas = map[string]string{
|
||||||
"user": "user13",
|
"user": "user13",
|
||||||
"repo": "repo11",
|
"repo": "repo11",
|
||||||
"repoPath": "../../tests/gitea-repositories-meta/user13/repo11.git/",
|
"repoPath": "../../tests/gitea-repositories-meta/user13/repo11.git/",
|
||||||
"mode": "comment",
|
"markdownLineBreakStyle": "comment",
|
||||||
|
"markupAllowShortIssuePattern": "true",
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestMain(m *testing.M) {
|
func TestMain(m *testing.M) {
|
||||||
|
@ -72,9 +74,9 @@ 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
|
|
||||||
}
|
}
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
|
@ -106,7 +108,7 @@ func TestRenderCommitBody(t *testing.T) {
|
||||||
ut := newTestRenderUtils()
|
ut := newTestRenderUtils()
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
assert.Equalf(t, tt.want, ut.RenderCommitBody(tt.args.msg, tt.args.metas), "RenderCommitBody(%v, %v)", tt.args.msg, tt.args.metas)
|
assert.Equalf(t, tt.want, ut.RenderCommitBody(tt.args.msg, nil), "RenderCommitBody(%v, %v)", tt.args.msg, nil)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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="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
|
||||||
|
@ -164,15 +164,15 @@ com 88fc37a3c0a4dda553bdcfc80c178a58247f42fb mit
|
||||||
<span class="emoji" aria-label="thumbs up">👍</span>
|
<span class="emoji" aria-label="thumbs up">👍</span>
|
||||||
mail@domain.com
|
mail@domain.com
|
||||||
@mention-user test
|
@mention-user test
|
||||||
<a href="/user13/repo11/issues/123" class="ref-issue">#123</a>
|
#123
|
||||||
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(), nil)))
|
||||||
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) {
|
||||||
|
|
|
@ -19,9 +19,10 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
prefix string
|
prefix string
|
||||||
SlowTest = 10 * time.Second
|
TestTimeout = 10 * time.Minute
|
||||||
SlowFlush = 5 * time.Second
|
TestSlowRun = 10 * time.Second
|
||||||
|
TestSlowFlush = 1 * time.Second
|
||||||
)
|
)
|
||||||
|
|
||||||
var WriterCloser = &testLoggerWriterCloser{}
|
var WriterCloser = &testLoggerWriterCloser{}
|
||||||
|
@ -89,79 +90,97 @@ func (w *testLoggerWriterCloser) Reset() {
|
||||||
w.Unlock()
|
w.Unlock()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Printf takes a format and args and prints the string to os.Stdout
|
||||||
|
func Printf(format string, args ...any) {
|
||||||
|
if !log.CanColorStdout {
|
||||||
|
for i := 0; i < len(args); i++ {
|
||||||
|
if c, ok := args[i].(*log.ColoredValue); ok {
|
||||||
|
args[i] = c.Value()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_, _ = fmt.Fprintf(os.Stdout, format, args...)
|
||||||
|
}
|
||||||
|
|
||||||
// PrintCurrentTest prints the current test to os.Stdout
|
// PrintCurrentTest prints the current test to os.Stdout
|
||||||
func PrintCurrentTest(t testing.TB, skip ...int) func() {
|
func PrintCurrentTest(t testing.TB, skip ...int) func() {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
start := time.Now()
|
runStart := time.Now()
|
||||||
actualSkip := util.OptionalArg(skip) + 1
|
actualSkip := util.OptionalArg(skip) + 1
|
||||||
_, filename, line, _ := runtime.Caller(actualSkip)
|
_, filename, line, _ := runtime.Caller(actualSkip)
|
||||||
|
|
||||||
if log.CanColorStdout {
|
Printf("=== %s (%s:%d)\n", log.NewColoredValue(t.Name()), strings.TrimPrefix(filename, prefix), line)
|
||||||
_, _ = fmt.Fprintf(os.Stdout, "=== %s (%s:%d)\n", fmt.Formatter(log.NewColoredValue(t.Name())), strings.TrimPrefix(filename, prefix), line)
|
|
||||||
} else {
|
|
||||||
_, _ = fmt.Fprintf(os.Stdout, "=== %s (%s:%d)\n", t.Name(), strings.TrimPrefix(filename, prefix), line)
|
|
||||||
}
|
|
||||||
WriterCloser.pushT(t)
|
WriterCloser.pushT(t)
|
||||||
return func() {
|
timeoutChecker := time.AfterFunc(TestTimeout, func() {
|
||||||
took := time.Since(start)
|
l := 128 * 1024
|
||||||
if took > SlowTest {
|
var stack []byte
|
||||||
if log.CanColorStdout {
|
for {
|
||||||
_, _ = fmt.Fprintf(os.Stdout, "+++ %s is a slow test (took %v)\n", fmt.Formatter(log.NewColoredValue(t.Name(), log.Bold, log.FgYellow)), fmt.Formatter(log.NewColoredValue(took, log.Bold, log.FgYellow)))
|
stack = make([]byte, l)
|
||||||
} else {
|
n := runtime.Stack(stack, true)
|
||||||
_, _ = fmt.Fprintf(os.Stdout, "+++ %s is a slow test (took %v)\n", t.Name(), took)
|
if n <= l {
|
||||||
|
stack = stack[:n]
|
||||||
|
break
|
||||||
}
|
}
|
||||||
|
l = n
|
||||||
}
|
}
|
||||||
timer := time.AfterFunc(SlowFlush, func() {
|
Printf("!!! %s ... timeout: %v ... stacktrace:\n%s\n\n", log.NewColoredValue(t.Name(), log.Bold, log.FgRed), TestTimeout, string(stack))
|
||||||
if log.CanColorStdout {
|
})
|
||||||
_, _ = fmt.Fprintf(os.Stdout, "+++ %s ... still flushing after %v ...\n", fmt.Formatter(log.NewColoredValue(t.Name(), log.Bold, log.FgRed)), SlowFlush)
|
return func() {
|
||||||
} else {
|
flushStart := time.Now()
|
||||||
_, _ = fmt.Fprintf(os.Stdout, "+++ %s ... still flushing after %v ...\n", t.Name(), SlowFlush)
|
slowFlushChecker := time.AfterFunc(TestSlowFlush, func() {
|
||||||
}
|
Printf("+++ %s ... still flushing after %v ...\n", log.NewColoredValue(t.Name(), log.Bold, log.FgRed), TestSlowFlush)
|
||||||
})
|
})
|
||||||
if err := queue.GetManager().FlushAll(context.Background(), time.Minute); err != nil {
|
if err := queue.GetManager().FlushAll(context.Background(), -1); err != nil {
|
||||||
t.Errorf("Flushing queues failed with error %v", err)
|
t.Errorf("Flushing queues failed with error %v", err)
|
||||||
}
|
}
|
||||||
timer.Stop()
|
slowFlushChecker.Stop()
|
||||||
flushTook := time.Since(start) - took
|
timeoutChecker.Stop()
|
||||||
if flushTook > SlowFlush {
|
|
||||||
if log.CanColorStdout {
|
runDuration := time.Since(runStart)
|
||||||
_, _ = fmt.Fprintf(os.Stdout, "+++ %s had a slow clean-up flush (took %v)\n", fmt.Formatter(log.NewColoredValue(t.Name(), log.Bold, log.FgRed)), fmt.Formatter(log.NewColoredValue(flushTook, log.Bold, log.FgRed)))
|
flushDuration := time.Since(flushStart)
|
||||||
} else {
|
if runDuration > TestSlowRun {
|
||||||
_, _ = fmt.Fprintf(os.Stdout, "+++ %s had a slow clean-up flush (took %v)\n", t.Name(), flushTook)
|
Printf("+++ %s is a slow test (run: %v, flush: %v)\n", log.NewColoredValue(t.Name(), log.Bold, log.FgYellow), runDuration, flushDuration)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
WriterCloser.popT()
|
WriterCloser.popT()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Printf takes a format and args and prints the string to os.Stdout
|
|
||||||
func Printf(format string, args ...any) {
|
|
||||||
if log.CanColorStdout {
|
|
||||||
for i := 0; i < len(args); i++ {
|
|
||||||
args[i] = log.NewColoredValue(args[i])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
_, _ = fmt.Fprintf(os.Stdout, "\t"+format, args...)
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestLogEventWriter is a logger which will write to the testing log
|
// TestLogEventWriter is a logger which will write to the testing log
|
||||||
type TestLogEventWriter struct {
|
type TestLogEventWriter struct {
|
||||||
*log.EventWriterBaseImpl
|
*log.EventWriterBaseImpl
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewTestLoggerWriter creates a TestLogEventWriter as a log.LoggerProvider
|
// newTestLoggerWriter creates a TestLogEventWriter as a log.LoggerProvider
|
||||||
func NewTestLoggerWriter(name string, mode log.WriterMode) log.EventWriter {
|
func newTestLoggerWriter(name string, mode log.WriterMode) log.EventWriter {
|
||||||
w := &TestLogEventWriter{}
|
w := &TestLogEventWriter{}
|
||||||
w.EventWriterBaseImpl = log.NewEventWriterBase(name, "test-log-writer", mode)
|
w.EventWriterBaseImpl = log.NewEventWriterBase(name, "test-log-writer", mode)
|
||||||
w.OutputWriteCloser = WriterCloser
|
w.OutputWriteCloser = WriterCloser
|
||||||
return w
|
return w
|
||||||
}
|
}
|
||||||
|
|
||||||
func init() {
|
func Init() {
|
||||||
const relFilePath = "modules/testlogger/testlogger.go"
|
const relFilePath = "modules/testlogger/testlogger.go"
|
||||||
_, filename, _, _ := runtime.Caller(0)
|
_, filename, _, _ := runtime.Caller(0)
|
||||||
if !strings.HasSuffix(filename, relFilePath) {
|
if !strings.HasSuffix(filename, relFilePath) {
|
||||||
panic("source code file path doesn't match expected: " + relFilePath)
|
panic("source code file path doesn't match expected: " + relFilePath)
|
||||||
}
|
}
|
||||||
prefix = strings.TrimSuffix(filename, relFilePath)
|
prefix = strings.TrimSuffix(filename, relFilePath)
|
||||||
|
|
||||||
|
log.RegisterEventWriter("test", newTestLoggerWriter)
|
||||||
|
|
||||||
|
duration, err := time.ParseDuration(os.Getenv("GITEA_TEST_SLOW_RUN"))
|
||||||
|
if err == nil && duration > 0 {
|
||||||
|
TestSlowRun = duration
|
||||||
|
}
|
||||||
|
|
||||||
|
duration, err = time.ParseDuration(os.Getenv("GITEA_TEST_SLOW_FLUSH"))
|
||||||
|
if err == nil && duration > 0 {
|
||||||
|
TestSlowFlush = duration
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func Fatalf(format string, args ...any) {
|
||||||
|
Printf(format+"\n", args...)
|
||||||
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,10 +5,12 @@ package typesniffer
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
|
"encoding/binary"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
"regexp"
|
"regexp"
|
||||||
|
"slices"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"code.gitea.io/gitea/modules/util"
|
"code.gitea.io/gitea/modules/util"
|
||||||
|
@ -18,10 +20,10 @@ import (
|
||||||
const sniffLen = 1024
|
const sniffLen = 1024
|
||||||
|
|
||||||
const (
|
const (
|
||||||
// SvgMimeType MIME type of SVG images.
|
MimeTypeImageSvg = "image/svg+xml"
|
||||||
SvgMimeType = "image/svg+xml"
|
MimeTypeImageAvif = "image/avif"
|
||||||
// ApplicationOctetStream MIME type of binary files.
|
|
||||||
ApplicationOctetStream = "application/octet-stream"
|
MimeTypeApplicationOctetStream = "application/octet-stream"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
|
@ -47,7 +49,7 @@ func (ct SniffedType) IsImage() bool {
|
||||||
|
|
||||||
// IsSvgImage detects if data is an SVG image format
|
// IsSvgImage detects if data is an SVG image format
|
||||||
func (ct SniffedType) IsSvgImage() bool {
|
func (ct SniffedType) IsSvgImage() bool {
|
||||||
return strings.Contains(ct.contentType, SvgMimeType)
|
return strings.Contains(ct.contentType, MimeTypeImageSvg)
|
||||||
}
|
}
|
||||||
|
|
||||||
// IsPDF detects if data is a PDF format
|
// IsPDF detects if data is a PDF format
|
||||||
|
@ -81,6 +83,26 @@ func (ct SniffedType) GetMimeType() string {
|
||||||
return strings.SplitN(ct.contentType, ";", 2)[0]
|
return strings.SplitN(ct.contentType, ";", 2)[0]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// https://en.wikipedia.org/wiki/ISO_base_media_file_format#File_type_box
|
||||||
|
func detectFileTypeBox(data []byte) (brands []string, found bool) {
|
||||||
|
if len(data) < 12 {
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
boxSize := int(binary.BigEndian.Uint32(data[:4]))
|
||||||
|
if boxSize < 12 || boxSize > len(data) {
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
tag := string(data[4:8])
|
||||||
|
if tag != "ftyp" {
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
brands = append(brands, string(data[8:12]))
|
||||||
|
for i := 16; i+4 <= boxSize; i += 4 {
|
||||||
|
brands = append(brands, string(data[i:i+4]))
|
||||||
|
}
|
||||||
|
return brands, true
|
||||||
|
}
|
||||||
|
|
||||||
// DetectContentType extends http.DetectContentType with more content types. Defaults to text/unknown if input is empty.
|
// DetectContentType extends http.DetectContentType with more content types. Defaults to text/unknown if input is empty.
|
||||||
func DetectContentType(data []byte) SniffedType {
|
func DetectContentType(data []byte) SniffedType {
|
||||||
if len(data) == 0 {
|
if len(data) == 0 {
|
||||||
|
@ -94,7 +116,6 @@ func DetectContentType(data []byte) SniffedType {
|
||||||
}
|
}
|
||||||
|
|
||||||
// SVG is unsupported by http.DetectContentType, https://github.com/golang/go/issues/15888
|
// SVG is unsupported by http.DetectContentType, https://github.com/golang/go/issues/15888
|
||||||
|
|
||||||
detectByHTML := strings.Contains(ct, "text/plain") || strings.Contains(ct, "text/html")
|
detectByHTML := strings.Contains(ct, "text/plain") || strings.Contains(ct, "text/html")
|
||||||
detectByXML := strings.Contains(ct, "text/xml")
|
detectByXML := strings.Contains(ct, "text/xml")
|
||||||
if detectByHTML || detectByXML {
|
if detectByHTML || detectByXML {
|
||||||
|
@ -102,7 +123,7 @@ func DetectContentType(data []byte) SniffedType {
|
||||||
dataProcessed = bytes.TrimSpace(dataProcessed)
|
dataProcessed = bytes.TrimSpace(dataProcessed)
|
||||||
if detectByHTML && svgTagRegex.Match(dataProcessed) ||
|
if detectByHTML && svgTagRegex.Match(dataProcessed) ||
|
||||||
detectByXML && svgTagInXMLRegex.Match(dataProcessed) {
|
detectByXML && svgTagInXMLRegex.Match(dataProcessed) {
|
||||||
ct = SvgMimeType
|
ct = MimeTypeImageSvg
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -116,6 +137,11 @@ func DetectContentType(data []byte) SniffedType {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fileTypeBrands, found := detectFileTypeBox(data)
|
||||||
|
if found && slices.Contains(fileTypeBrands, "avif") {
|
||||||
|
ct = MimeTypeImageAvif
|
||||||
|
}
|
||||||
|
|
||||||
if ct == "application/ogg" {
|
if ct == "application/ogg" {
|
||||||
dataHead := data
|
dataHead := data
|
||||||
if len(dataHead) > 256 {
|
if len(dataHead) > 256 {
|
||||||
|
|
|
@ -134,3 +134,33 @@ func TestDetectContentTypeOgg(t *testing.T) {
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
assert.True(t, st.IsVideo())
|
assert.True(t, st.IsVideo())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestDetectFileTypeBox(t *testing.T) {
|
||||||
|
_, found := detectFileTypeBox([]byte("\x00\x00\xff\xffftypAAAA...."))
|
||||||
|
assert.False(t, found)
|
||||||
|
|
||||||
|
brands, found := detectFileTypeBox([]byte("\x00\x00\x00\x0cftypAAAA"))
|
||||||
|
assert.True(t, found)
|
||||||
|
assert.Equal(t, []string{"AAAA"}, brands)
|
||||||
|
|
||||||
|
brands, found = detectFileTypeBox([]byte("\x00\x00\x00\x10ftypAAAA....BBBB"))
|
||||||
|
assert.True(t, found)
|
||||||
|
assert.Equal(t, []string{"AAAA"}, brands)
|
||||||
|
|
||||||
|
brands, found = detectFileTypeBox([]byte("\x00\x00\x00\x14ftypAAAA....BBBB"))
|
||||||
|
assert.True(t, found)
|
||||||
|
assert.Equal(t, []string{"AAAA", "BBBB"}, brands)
|
||||||
|
|
||||||
|
_, found = detectFileTypeBox([]byte("\x00\x00\x00\x14ftypAAAA....BBB"))
|
||||||
|
assert.False(t, found)
|
||||||
|
|
||||||
|
brands, found = detectFileTypeBox([]byte("\x00\x00\x00\x13ftypAAAA....BBB"))
|
||||||
|
assert.True(t, found)
|
||||||
|
assert.Equal(t, []string{"AAAA"}, brands)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDetectContentTypeAvif(t *testing.T) {
|
||||||
|
buf := []byte("\x00\x00\x00\x20ftypavif.......................")
|
||||||
|
st := DetectContentType(buf)
|
||||||
|
assert.Equal(t, MimeTypeImageAvif, st.contentType)
|
||||||
|
}
|
||||||
|
|
|
@ -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,34 @@ 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.Metas = map[string]string{"markdownLineBreakStyle": "comment"}
|
||||||
markupType = markdown.MarkupName
|
case "wiki":
|
||||||
case "gfm":
|
renderCtx.Metas = map[string]string{"markdownLineBreakStyle": "document", "markupContentMode": "wiki"}
|
||||||
// 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.Metas = map[string]string{"markdownLineBreakStyle": "document"}
|
||||||
|
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 +70,21 @@ 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 mode == "file" {
|
||||||
meta = repo.Repository.ComposeMetas(ctx)
|
renderCtx.Metas = repo.Repository.ComposeDocumentMetas(ctx)
|
||||||
} else {
|
} else if mode == "wiki" {
|
||||||
meta = repo.Repository.ComposeDocumentMetas(ctx)
|
renderCtx.Metas = repo.Repository.ComposeWikiMetas(ctx)
|
||||||
|
} else if mode == "comment" {
|
||||||
|
renderCtx.Metas = repo.Repository.ComposeMetas(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())
|
||||||
|
|
|
@ -122,6 +122,8 @@ func SignInOAuthCallback(ctx *context.Context) {
|
||||||
}
|
}
|
||||||
if err, ok := err.(*go_oauth2.RetrieveError); ok {
|
if err, ok := err.(*go_oauth2.RetrieveError); ok {
|
||||||
ctx.Flash.Error("OAuth2 RetrieveError: "+err.Error(), true)
|
ctx.Flash.Error("OAuth2 RetrieveError: "+err.Error(), true)
|
||||||
|
ctx.Redirect(setting.AppSubURL + "/user/login")
|
||||||
|
return
|
||||||
}
|
}
|
||||||
ctx.ServerError("UserSignIn", err)
|
ctx.ServerError("UserSignIn", err)
|
||||||
return
|
return
|
||||||
|
|
|
@ -56,8 +56,7 @@ 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{ // FIXME: not right here, it should use issue to compose the metas
|
||||||
Metas: map[string]string{
|
|
||||||
"user": act.GetRepoUserName(ctx),
|
"user": act.GetRepoUserName(ctx),
|
||||||
"repo": act.GetRepoName(ctx),
|
"repo": act.GetRepoName(ctx),
|
||||||
},
|
},
|
||||||
|
|
|
@ -46,9 +46,7 @@ func showUserFeed(ctx *context.Context, formatType string) {
|
||||||
Links: markup.Links{
|
Links: markup.Links{
|
||||||
Base: ctx.ContextUser.HTMLURL(),
|
Base: ctx.ContextUser.HTMLURL(),
|
||||||
},
|
},
|
||||||
Metas: map[string]string{
|
Metas: markup.ComposeSimpleDocumentMetas(),
|
||||||
"user": ctx.ContextUser.GetDisplayName(),
|
|
||||||
},
|
|
||||||
}, ctx.ContextUser.Description)
|
}, ctx.ContextUser.Description)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ctx.ServerError("RenderString", err)
|
ctx.ServerError("RenderString", err)
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
@ -189,7 +189,7 @@ func prepareOrgProfileReadme(ctx *context.Context, viewRepositories bool) bool {
|
||||||
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"},
|
Metas: markup.ComposeSimpleDocumentMetas(),
|
||||||
}, bytes); err != nil {
|
}, bytes); err != nil {
|
||||||
log.Error("failed to RenderString: %v", err)
|
log.Error("failed to RenderString: %v", err)
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -290,11 +290,10 @@ 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),
|
Metas: ctx.Repo.Repository.ComposeWikiMetas(ctx),
|
||||||
Links: markup.Links{
|
Links: markup.Links{
|
||||||
Base: ctx.Repo.RepoLink,
|
Base: ctx.Repo.RepoLink,
|
||||||
},
|
},
|
||||||
IsWiki: true,
|
|
||||||
}
|
}
|
||||||
buf := &strings.Builder{}
|
buf := &strings.Builder{}
|
||||||
|
|
||||||
|
|
|
@ -50,7 +50,7 @@ func PrepareContextForProfileBigAvatar(ctx *context.Context) {
|
||||||
ctx.Data["OpenIDs"] = openIDs
|
ctx.Data["OpenIDs"] = openIDs
|
||||||
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"},
|
Metas: markup.ComposeSimpleDocumentMetas(),
|
||||||
Ctx: ctx,
|
Ctx: ctx,
|
||||||
}, ctx.ContextUser.Description)
|
}, ctx.ContextUser.Description)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -5,6 +5,7 @@
|
||||||
package auth
|
package auth
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"errors"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
@ -141,6 +142,15 @@ func (b *Basic) Verify(req *http.Request, w http.ResponseWriter, store DataStore
|
||||||
}
|
}
|
||||||
|
|
||||||
if skipper, ok := source.Cfg.(LocalTwoFASkipper); !ok || !skipper.IsSkipLocalTwoFA() {
|
if skipper, ok := source.Cfg.(LocalTwoFASkipper); !ok || !skipper.IsSkipLocalTwoFA() {
|
||||||
|
// Check if the user has webAuthn registration
|
||||||
|
hasWebAuthn, err := auth_model.HasWebAuthnRegistrationsByUID(req.Context(), u.ID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if hasWebAuthn {
|
||||||
|
return nil, errors.New("Basic authorization is not allowed while webAuthn enrolled")
|
||||||
|
}
|
||||||
|
|
||||||
if err := validateTOTP(req, u); err != nil {
|
if err := validateTOTP(req, u); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -1 +0,0 @@
|
||||||
Unnamed repository; edit this file 'description' to name the repository.
|
|
|
@ -1,6 +0,0 @@
|
||||||
# git ls-files --others --exclude-from=.git/info/exclude
|
|
||||||
# Lines that start with '#' are comments.
|
|
||||||
# For a project mostly in C, the following would be a good set of
|
|
||||||
# exclude patterns (uncomment them if you want to use them):
|
|
||||||
# *.[oa]
|
|
||||||
# *~
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue